mdorigin 0.1.7 → 0.2.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/README.md +12 -0
- package/dist/adapters/cloudflare.d.ts +40 -4
- package/dist/adapters/cloudflare.js +163 -7
- package/dist/adapters/node.js +2 -2
- package/dist/cli/build-cloudflare.js +27 -2
- package/dist/cli/help.js +5 -4
- package/dist/cli/init-cloudflare.js +7 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/sync-cloudflare-r2.d.ts +1 -0
- package/dist/cli/sync-cloudflare-r2.js +47 -0
- package/dist/cloudflare-runtime.d.ts +1 -1
- package/dist/cloudflare.d.ts +42 -6
- package/dist/cloudflare.js +246 -10
- package/dist/core/content-store.d.ts +1 -0
- package/dist/core/content-store.js +8 -1
- package/dist/core/extensions.d.ts +5 -9
- package/dist/core/markdown.js +32 -4
- package/dist/core/request-handler.js +46 -60
- package/dist/core/site-config.d.ts +4 -10
- package/dist/core/site-config.js +18 -10
- package/dist/html/template.d.ts +5 -9
- package/dist/html/template.js +33 -27
- package/dist/html/theme.d.ts +1 -2
- package/dist/html/theme.js +12 -628
- package/dist/index-builder.js +6 -4
- package/dist/search.js +3 -2
- package/package.json +4 -2
- package/dist/html/template-kind.d.ts +0 -1
- package/dist/html/template-kind.js +0 -1
package/dist/cloudflare.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import { copyFile, mkdir, readFile, readdir, realpath, rm, stat, writeFile, } from 'node:fs/promises';
|
|
2
5
|
import path from 'node:path';
|
|
3
6
|
import { createCloudflareWorker } from './adapters/cloudflare.js';
|
|
4
|
-
import { getMediaTypeForPath, isLikelyTextPath, normalizeContentPath, } from './core/content-store.js';
|
|
7
|
+
import { getMediaTypeForPath, isIgnoredContentName, isLikelyTextPath, normalizeContentPath, } from './core/content-store.js';
|
|
5
8
|
export { createCloudflareWorker };
|
|
9
|
+
const DEFAULT_ASSETS_MAX_BYTES = 25 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_ASSETS_BINDING = 'ASSETS';
|
|
11
|
+
const DEFAULT_R2_BINDING = 'MDORIGIN_R2';
|
|
12
|
+
const BUNDLE_FILE_NAME = 'bundle.json';
|
|
13
|
+
const R2_STATE_FILE_NAME = 'r2-sync-state.json';
|
|
6
14
|
export async function buildCloudflareManifest(options) {
|
|
7
15
|
const rootDir = path.resolve(options.rootDir);
|
|
8
16
|
const files = await listFiles(rootDir);
|
|
9
17
|
const entries = [];
|
|
18
|
+
const binaryMode = options.binaryMode ?? 'inline';
|
|
19
|
+
const assetsMaxBytes = options.assetsMaxBytes ?? DEFAULT_ASSETS_MAX_BYTES;
|
|
20
|
+
const r2Binding = options.r2Binding ?? DEFAULT_R2_BINDING;
|
|
10
21
|
for (const filePath of files) {
|
|
11
22
|
const relativePath = path.relative(rootDir, filePath).replaceAll(path.sep, '/');
|
|
12
23
|
const normalizedPath = normalizeContentPath(relativePath);
|
|
@@ -23,12 +34,21 @@ export async function buildCloudflareManifest(options) {
|
|
|
23
34
|
});
|
|
24
35
|
continue;
|
|
25
36
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
if (binaryMode === 'inline') {
|
|
38
|
+
const bytes = await readFile(filePath);
|
|
39
|
+
entries.push({
|
|
40
|
+
path: normalizedPath,
|
|
41
|
+
kind: 'binary',
|
|
42
|
+
mediaType,
|
|
43
|
+
base64: bytes.toString('base64'),
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const fileStats = await stat(filePath);
|
|
48
|
+
entries.push(await buildExternalBinaryEntry(filePath, normalizedPath, mediaType, fileStats.size, {
|
|
49
|
+
assetsMaxBytes,
|
|
50
|
+
r2Binding,
|
|
51
|
+
}));
|
|
32
52
|
}
|
|
33
53
|
const searchEntries = options.searchDir
|
|
34
54
|
? await readBundleEntries(path.resolve(options.searchDir))
|
|
@@ -38,17 +58,32 @@ export async function buildCloudflareManifest(options) {
|
|
|
38
58
|
entries,
|
|
39
59
|
siteConfig: options.siteConfig,
|
|
40
60
|
searchEntries,
|
|
61
|
+
runtime: binaryMode === 'external'
|
|
62
|
+
? {
|
|
63
|
+
binaryMode,
|
|
64
|
+
r2Binding,
|
|
65
|
+
}
|
|
66
|
+
: {
|
|
67
|
+
binaryMode,
|
|
68
|
+
},
|
|
41
69
|
};
|
|
42
70
|
}
|
|
43
71
|
export async function writeCloudflareBundle(options) {
|
|
44
72
|
const outDir = path.resolve(options.outDir);
|
|
73
|
+
const binaryMode = options.binaryMode ?? 'inline';
|
|
74
|
+
const assetsMaxBytes = options.assetsMaxBytes ?? DEFAULT_ASSETS_MAX_BYTES;
|
|
75
|
+
const r2Binding = options.r2Binding ?? DEFAULT_R2_BINDING;
|
|
45
76
|
const manifest = await buildCloudflareManifest({
|
|
46
77
|
rootDir: options.rootDir,
|
|
47
78
|
siteConfig: options.siteConfig,
|
|
48
79
|
searchDir: options.searchDir,
|
|
80
|
+
binaryMode,
|
|
81
|
+
assetsMaxBytes,
|
|
82
|
+
r2Binding,
|
|
49
83
|
});
|
|
50
84
|
const packageImport = options.packageImport ?? 'mdorigin/cloudflare-runtime';
|
|
51
85
|
const workerFile = path.join(outDir, 'worker.mjs');
|
|
86
|
+
const bundleFile = path.join(outDir, BUNDLE_FILE_NAME);
|
|
52
87
|
const configImportPath = options.configModulePath
|
|
53
88
|
? toPosixPath(path.relative(outDir, options.configModulePath))
|
|
54
89
|
: null;
|
|
@@ -83,10 +118,18 @@ export async function writeCloudflareBundle(options) {
|
|
|
83
118
|
'',
|
|
84
119
|
].join('\n');
|
|
85
120
|
await mkdir(outDir, { recursive: true });
|
|
121
|
+
const metadata = await writeExternalBinaryStaging(path.resolve(options.rootDir), outDir, manifest, {
|
|
122
|
+
binaryMode,
|
|
123
|
+
assetsMaxBytes,
|
|
124
|
+
r2Binding,
|
|
125
|
+
siteTitle: options.siteConfig.siteTitle,
|
|
126
|
+
});
|
|
86
127
|
await writeFile(workerFile, workerSource, 'utf8');
|
|
128
|
+
await writeFile(bundleFile, JSON.stringify(metadata, null, 2), 'utf8');
|
|
87
129
|
return {
|
|
88
130
|
manifest,
|
|
89
131
|
workerFile,
|
|
132
|
+
bundleFile,
|
|
90
133
|
};
|
|
91
134
|
}
|
|
92
135
|
export async function initCloudflareProject(options) {
|
|
@@ -96,8 +139,14 @@ export async function initCloudflareProject(options) {
|
|
|
96
139
|
if (existing && !options.force) {
|
|
97
140
|
throw new Error(`Refusing to overwrite ${configFile}. Re-run with --force to replace it.`);
|
|
98
141
|
}
|
|
142
|
+
const bundleMetadata = await readCloudflareBundleMetadata(options.workerEntry);
|
|
143
|
+
if (bundleMetadata?.binaryMode === 'external' &&
|
|
144
|
+
bundleMetadata.r2Objects.length > 0 &&
|
|
145
|
+
!options.r2Bucket) {
|
|
146
|
+
throw new Error('Cloudflare bundle contains R2-backed binaries. Re-run init cloudflare with --r2-bucket <bucket-name>.');
|
|
147
|
+
}
|
|
99
148
|
const workerName = options.workerName ??
|
|
100
|
-
slugifyWorkerName(options.siteTitle) ??
|
|
149
|
+
slugifyWorkerName(bundleMetadata?.siteTitle ?? options.siteTitle) ??
|
|
101
150
|
'mdorigin-site';
|
|
102
151
|
const compatibilityDate = options.compatibilityDate ?? '2026-03-20';
|
|
103
152
|
const wranglerConfig = [
|
|
@@ -107,13 +156,197 @@ export async function initCloudflareProject(options) {
|
|
|
107
156
|
` "main": ${JSON.stringify(toPosixPath(path.relative(projectDir, options.workerEntry)))},`,
|
|
108
157
|
` "compatibility_date": ${JSON.stringify(compatibilityDate)},`,
|
|
109
158
|
' "compatibility_flags": ["nodejs_compat"]',
|
|
159
|
+
bundleMetadata?.binaryMode === 'external' && bundleMetadata.assetsDir
|
|
160
|
+
? [
|
|
161
|
+
',',
|
|
162
|
+
' "assets": {',
|
|
163
|
+
` "directory": ${JSON.stringify(toPosixPath(path.relative(projectDir, path.join(path.dirname(options.workerEntry), bundleMetadata.assetsDir))))},`,
|
|
164
|
+
` "binding": ${JSON.stringify(DEFAULT_ASSETS_BINDING)},`,
|
|
165
|
+
' "run_worker_first": true',
|
|
166
|
+
' }',
|
|
167
|
+
].join('\n')
|
|
168
|
+
: '',
|
|
169
|
+
bundleMetadata?.binaryMode === 'external' &&
|
|
170
|
+
bundleMetadata.r2Objects.length > 0 &&
|
|
171
|
+
bundleMetadata.r2Binding &&
|
|
172
|
+
options.r2Bucket
|
|
173
|
+
? [
|
|
174
|
+
',',
|
|
175
|
+
' "r2_buckets": [',
|
|
176
|
+
' {',
|
|
177
|
+
` "binding": ${JSON.stringify(bundleMetadata.r2Binding)},`,
|
|
178
|
+
` "bucket_name": ${JSON.stringify(options.r2Bucket)}`,
|
|
179
|
+
' }',
|
|
180
|
+
' ]',
|
|
181
|
+
].join('\n')
|
|
182
|
+
: '',
|
|
110
183
|
'}',
|
|
111
184
|
'',
|
|
112
|
-
]
|
|
185
|
+
]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join('\n');
|
|
113
188
|
await mkdir(projectDir, { recursive: true });
|
|
114
189
|
await writeFile(configFile, wranglerConfig, 'utf8');
|
|
115
190
|
return { configFile };
|
|
116
191
|
}
|
|
192
|
+
export async function syncCloudflareR2(options) {
|
|
193
|
+
const outDir = path.resolve(options.dir);
|
|
194
|
+
const bundleFile = path.join(outDir, BUNDLE_FILE_NAME);
|
|
195
|
+
const metadata = await readBundleMetadataFile(bundleFile);
|
|
196
|
+
if (metadata.binaryMode !== 'external' || metadata.r2Objects.length === 0) {
|
|
197
|
+
throw new Error(`No R2-backed binaries found in ${bundleFile}.`);
|
|
198
|
+
}
|
|
199
|
+
const stateFile = path.join(outDir, R2_STATE_FILE_NAME);
|
|
200
|
+
const state = await readR2SyncState(stateFile);
|
|
201
|
+
const runCommand = options.runCommand ?? runWranglerCommand;
|
|
202
|
+
let uploadedCount = 0;
|
|
203
|
+
let skippedCount = 0;
|
|
204
|
+
for (const object of metadata.r2Objects) {
|
|
205
|
+
const stateKey = `${options.bucketName}:${object.storageKey}`;
|
|
206
|
+
if (!options.force && state.uploaded[stateKey]) {
|
|
207
|
+
skippedCount += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const filePath = path.join(outDir, object.file);
|
|
211
|
+
const result = runCommand('wrangler', [
|
|
212
|
+
'r2',
|
|
213
|
+
'object',
|
|
214
|
+
'put',
|
|
215
|
+
`${options.bucketName}/${object.storageKey}`,
|
|
216
|
+
'--file',
|
|
217
|
+
filePath,
|
|
218
|
+
'--content-type',
|
|
219
|
+
object.mediaType,
|
|
220
|
+
'--remote',
|
|
221
|
+
]);
|
|
222
|
+
if (result.status !== 0) {
|
|
223
|
+
throw new Error(result.stderr || `Failed to upload R2 object ${object.storageKey}.`);
|
|
224
|
+
}
|
|
225
|
+
state.uploaded[stateKey] = {
|
|
226
|
+
syncedAt: new Date().toISOString(),
|
|
227
|
+
};
|
|
228
|
+
uploadedCount += 1;
|
|
229
|
+
}
|
|
230
|
+
await writeFile(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
231
|
+
return {
|
|
232
|
+
uploadedCount,
|
|
233
|
+
skippedCount,
|
|
234
|
+
stateFile,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function buildExternalBinaryEntry(filePath, normalizedPath, mediaType, byteSize, options) {
|
|
238
|
+
if (byteSize <= options.assetsMaxBytes) {
|
|
239
|
+
return {
|
|
240
|
+
path: normalizedPath,
|
|
241
|
+
kind: 'binary',
|
|
242
|
+
mediaType,
|
|
243
|
+
storageKind: 'assets',
|
|
244
|
+
storageKey: normalizedPath,
|
|
245
|
+
byteSize,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
path: normalizedPath,
|
|
250
|
+
kind: 'binary',
|
|
251
|
+
mediaType,
|
|
252
|
+
storageKind: 'r2',
|
|
253
|
+
storageKey: await buildR2StorageKey(filePath, normalizedPath),
|
|
254
|
+
byteSize,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function writeExternalBinaryStaging(rootDir, outDir, manifest, options) {
|
|
258
|
+
const assetsDir = path.join(outDir, 'assets');
|
|
259
|
+
const r2Dir = path.join(outDir, 'r2');
|
|
260
|
+
await rm(assetsDir, { recursive: true, force: true });
|
|
261
|
+
await rm(r2Dir, { recursive: true, force: true });
|
|
262
|
+
const r2Objects = new Map();
|
|
263
|
+
if (options.binaryMode === 'external') {
|
|
264
|
+
for (const entry of manifest.entries) {
|
|
265
|
+
if (entry.kind !== 'binary' || !('storageKind' in entry)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const sourceFile = path.resolve(rootDir, entry.path);
|
|
269
|
+
if (entry.storageKind === 'assets') {
|
|
270
|
+
const targetFile = path.join(assetsDir, entry.storageKey);
|
|
271
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
272
|
+
await copyFile(sourceFile, targetFile);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const relativeFile = toPosixPath(path.join('r2', entry.storageKey));
|
|
276
|
+
const targetFile = path.join(outDir, relativeFile);
|
|
277
|
+
if (!r2Objects.has(entry.storageKey)) {
|
|
278
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
279
|
+
await copyFile(sourceFile, targetFile);
|
|
280
|
+
r2Objects.set(entry.storageKey, {
|
|
281
|
+
path: entry.path,
|
|
282
|
+
mediaType: entry.mediaType,
|
|
283
|
+
storageKey: entry.storageKey,
|
|
284
|
+
file: relativeFile,
|
|
285
|
+
byteSize: entry.byteSize,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
version: 1,
|
|
292
|
+
workerEntry: 'worker.mjs',
|
|
293
|
+
binaryMode: options.binaryMode,
|
|
294
|
+
assetsMaxBytes: options.binaryMode === 'external' ? options.assetsMaxBytes : undefined,
|
|
295
|
+
assetsDir: options.binaryMode === 'external' ? 'assets' : undefined,
|
|
296
|
+
r2Dir: options.binaryMode === 'external' ? 'r2' : undefined,
|
|
297
|
+
r2Binding: options.binaryMode === 'external' && r2Objects.size > 0
|
|
298
|
+
? options.r2Binding
|
|
299
|
+
: undefined,
|
|
300
|
+
siteTitle: options.siteTitle,
|
|
301
|
+
r2Objects: Array.from(r2Objects.values()).sort((left, right) => left.storageKey.localeCompare(right.storageKey)),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function readCloudflareBundleMetadata(workerEntry) {
|
|
305
|
+
const bundleFile = path.join(path.dirname(workerEntry), BUNDLE_FILE_NAME);
|
|
306
|
+
if (!(await pathExists(bundleFile))) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return readBundleMetadataFile(bundleFile);
|
|
310
|
+
}
|
|
311
|
+
async function readBundleMetadataFile(bundleFile) {
|
|
312
|
+
return JSON.parse(await readFile(bundleFile, 'utf8'));
|
|
313
|
+
}
|
|
314
|
+
async function readR2SyncState(stateFile) {
|
|
315
|
+
if (!(await pathExists(stateFile))) {
|
|
316
|
+
return { version: 1, uploaded: {} };
|
|
317
|
+
}
|
|
318
|
+
return JSON.parse(await readFile(stateFile, 'utf8'));
|
|
319
|
+
}
|
|
320
|
+
async function buildR2StorageKey(filePath, normalizedPath) {
|
|
321
|
+
const extension = path.posix.extname(normalizedPath).toLowerCase();
|
|
322
|
+
const hash = await hashFile(filePath);
|
|
323
|
+
return extension ? `binary/${hash}${extension}` : `binary/${hash}`;
|
|
324
|
+
}
|
|
325
|
+
async function hashFile(filePath) {
|
|
326
|
+
const hash = createHash('sha256');
|
|
327
|
+
await new Promise((resolve, reject) => {
|
|
328
|
+
const stream = createReadStream(filePath);
|
|
329
|
+
stream.on('data', (chunk) => {
|
|
330
|
+
hash.update(chunk);
|
|
331
|
+
});
|
|
332
|
+
stream.on('end', () => resolve());
|
|
333
|
+
stream.on('error', reject);
|
|
334
|
+
});
|
|
335
|
+
return hash.digest('hex');
|
|
336
|
+
}
|
|
337
|
+
function runWranglerCommand(command, args) {
|
|
338
|
+
const result = spawnSync(command, args, {
|
|
339
|
+
encoding: 'utf8',
|
|
340
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
341
|
+
});
|
|
342
|
+
if (result.error) {
|
|
343
|
+
throw result.error;
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
status: result.status,
|
|
347
|
+
stderr: result.stderr ?? '',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
117
350
|
async function listFiles(directory, visitedRealDirectories = new Set()) {
|
|
118
351
|
const directoryRealPath = await realpath(directory);
|
|
119
352
|
if (visitedRealDirectories.has(directoryRealPath)) {
|
|
@@ -123,6 +356,9 @@ async function listFiles(directory, visitedRealDirectories = new Set()) {
|
|
|
123
356
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
124
357
|
const files = [];
|
|
125
358
|
for (const entry of entries) {
|
|
359
|
+
if (isIgnoredContentName(entry.name)) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
126
362
|
const fullPath = path.join(directory, entry.name);
|
|
127
363
|
const entryStats = await stat(fullPath);
|
|
128
364
|
if (entryStats.isDirectory()) {
|
|
@@ -15,6 +15,7 @@ export interface ContentStore {
|
|
|
15
15
|
get(contentPath: string): Promise<ContentEntry | null>;
|
|
16
16
|
listDirectory(contentPath: string): Promise<ContentDirectoryEntry[] | null>;
|
|
17
17
|
}
|
|
18
|
+
export declare function isIgnoredContentName(name: string): boolean;
|
|
18
19
|
export declare class MemoryContentStore implements ContentStore {
|
|
19
20
|
private readonly entries;
|
|
20
21
|
constructor(entries: Iterable<ContentEntry>);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
export function isIgnoredContentName(name) {
|
|
3
|
+
return name.startsWith('.');
|
|
4
|
+
}
|
|
2
5
|
export class MemoryContentStore {
|
|
3
6
|
entries;
|
|
4
7
|
constructor(entries) {
|
|
@@ -23,7 +26,7 @@ export class MemoryContentStore {
|
|
|
23
26
|
continue;
|
|
24
27
|
}
|
|
25
28
|
const [firstSegment, ...rest] = remainder.split('/');
|
|
26
|
-
if (firstSegment
|
|
29
|
+
if (isIgnoredContentName(firstSegment)) {
|
|
27
30
|
continue;
|
|
28
31
|
}
|
|
29
32
|
if (rest.length === 0) {
|
|
@@ -53,6 +56,7 @@ const MEDIA_TYPES = new Map([
|
|
|
53
56
|
['.js', 'text/javascript; charset=utf-8'],
|
|
54
57
|
['.json', 'application/json; charset=utf-8'],
|
|
55
58
|
['.md', 'text/markdown; charset=utf-8'],
|
|
59
|
+
['.mp4', 'video/mp4'],
|
|
56
60
|
['.pdf', 'application/pdf'],
|
|
57
61
|
['.py', 'text/plain; charset=utf-8'],
|
|
58
62
|
['.png', 'image/png'],
|
|
@@ -84,6 +88,9 @@ export function normalizeContentPath(inputPath) {
|
|
|
84
88
|
resolved.includes('/../')) {
|
|
85
89
|
return null;
|
|
86
90
|
}
|
|
91
|
+
if (resolved.split('/').some(isIgnoredContentName)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
87
94
|
return resolved;
|
|
88
95
|
}
|
|
89
96
|
export function normalizeDirectoryPath(inputPath) {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { ManagedIndexEntry, ParsedDocumentMeta } from './markdown.js';
|
|
2
2
|
import type { EditLinkConfig, ResolvedSiteConfig, SiteLogo, SiteNavItem, SiteSocialLink } from './site-config.js';
|
|
3
|
-
import type { TemplateName } from '../html/template-kind.js';
|
|
4
|
-
import type { BuiltInThemeName } from '../html/theme.js';
|
|
5
3
|
type MaybePromise<T> = T | Promise<T>;
|
|
6
4
|
export interface IndexTransformContext {
|
|
7
5
|
mode: 'build' | 'render';
|
|
@@ -11,7 +9,7 @@ export interface IndexTransformContext {
|
|
|
11
9
|
siteConfig?: ResolvedSiteConfig;
|
|
12
10
|
}
|
|
13
11
|
export interface PageRenderModel {
|
|
14
|
-
kind: '
|
|
12
|
+
kind: 'page' | 'listing';
|
|
15
13
|
requestPath: string;
|
|
16
14
|
sourcePath: string;
|
|
17
15
|
siteTitle: string;
|
|
@@ -27,8 +25,6 @@ export interface PageRenderModel {
|
|
|
27
25
|
date?: string;
|
|
28
26
|
showSummary: boolean;
|
|
29
27
|
showDate: boolean;
|
|
30
|
-
theme: BuiltInThemeName;
|
|
31
|
-
template: TemplateName;
|
|
32
28
|
topNav: SiteNavItem[];
|
|
33
29
|
footerNav: SiteNavItem[];
|
|
34
30
|
footerText?: string;
|
|
@@ -38,10 +34,10 @@ export interface PageRenderModel {
|
|
|
38
34
|
stylesheetContent?: string;
|
|
39
35
|
canonicalPath?: string;
|
|
40
36
|
alternateMarkdownPath?: string;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
listingEntries: ManagedIndexEntry[];
|
|
38
|
+
listingRequestPath: string;
|
|
39
|
+
listingInitialPostCount: number;
|
|
40
|
+
listingLoadMoreStep: number;
|
|
45
41
|
searchEnabled: boolean;
|
|
46
42
|
}
|
|
47
43
|
export interface RenderHookContext {
|
package/dist/core/markdown.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import matter from 'gray-matter';
|
|
2
2
|
import { remark } from 'remark';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
|
-
import
|
|
4
|
+
import remarkRehype from 'remark-rehype';
|
|
5
|
+
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import rehypeStringify from 'rehype-stringify';
|
|
5
7
|
export function getDocumentTitle(meta, body, fallback) {
|
|
6
8
|
return (firstNonEmptyString(meta.title, meta.name) ??
|
|
7
9
|
extractFirstHeading(body) ??
|
|
@@ -24,7 +26,9 @@ export async function parseMarkdownDocument(sourcePath, markdown) {
|
|
|
24
26
|
export async function renderMarkdown(markdown) {
|
|
25
27
|
const output = await remark()
|
|
26
28
|
.use(remarkGfm)
|
|
27
|
-
.use(
|
|
29
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
30
|
+
.use(rehypeRaw)
|
|
31
|
+
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
28
32
|
.process(markdown);
|
|
29
33
|
return String(output);
|
|
30
34
|
}
|
|
@@ -78,15 +82,39 @@ export function extractManagedIndexEntries(markdown) {
|
|
|
78
82
|
}
|
|
79
83
|
const rawHref = entryMatch[2];
|
|
80
84
|
const href = rewriteMarkdownHref(rawHref);
|
|
85
|
+
const explicitKind = extractManagedIndexKind(lines.slice(1));
|
|
81
86
|
entries.push({
|
|
82
|
-
kind: href.endsWith('/') ? 'directory' : 'article',
|
|
87
|
+
kind: explicitKind ?? (href.endsWith('/') ? 'directory' : 'article'),
|
|
83
88
|
title: entryMatch[1],
|
|
84
89
|
href,
|
|
85
|
-
detail: lines
|
|
90
|
+
detail: extractManagedIndexDetail(lines.slice(1)),
|
|
86
91
|
});
|
|
87
92
|
}
|
|
88
93
|
return entries;
|
|
89
94
|
}
|
|
95
|
+
function extractManagedIndexKind(lines) {
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
const match = trimmed.match(/^<!--\s*mdorigin:index\s+kind=(article|directory)\s*-->$/);
|
|
99
|
+
if (match) {
|
|
100
|
+
return match[1];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
function extractManagedIndexDetail(lines) {
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
if (trimmed === '') {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (/^<!--\s*mdorigin:index\s+kind=(article|directory)\s*-->$/.test(trimmed)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
return trimmed;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
90
118
|
function normalizeMeta(data) {
|
|
91
119
|
const meta = { ...data };
|
|
92
120
|
if (typeof data.title === 'string') {
|