mdorigin 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.
Files changed (42) hide show
  1. package/README.md +29 -0
  2. package/dist/adapters/cloudflare.d.ts +17 -0
  3. package/dist/adapters/cloudflare.js +53 -0
  4. package/dist/adapters/node.d.ts +11 -0
  5. package/dist/adapters/node.js +115 -0
  6. package/dist/cli/build-cloudflare.d.ts +1 -0
  7. package/dist/cli/build-cloudflare.js +48 -0
  8. package/dist/cli/build-index.d.ts +1 -0
  9. package/dist/cli/build-index.js +35 -0
  10. package/dist/cli/dev.d.ts +1 -0
  11. package/dist/cli/dev.js +53 -0
  12. package/dist/cli/init-cloudflare.d.ts +1 -0
  13. package/dist/cli/init-cloudflare.js +59 -0
  14. package/dist/cli/main.d.ts +1 -0
  15. package/dist/cli/main.js +38 -0
  16. package/dist/cloudflare-runtime.d.ts +2 -0
  17. package/dist/cloudflare-runtime.js +1 -0
  18. package/dist/cloudflare.d.ts +31 -0
  19. package/dist/cloudflare.js +130 -0
  20. package/dist/core/content-store.d.ts +27 -0
  21. package/dist/core/content-store.js +95 -0
  22. package/dist/core/content-type.d.ts +9 -0
  23. package/dist/core/content-type.js +19 -0
  24. package/dist/core/directory-index.d.ts +2 -0
  25. package/dist/core/directory-index.js +5 -0
  26. package/dist/core/markdown.d.ts +20 -0
  27. package/dist/core/markdown.js +135 -0
  28. package/dist/core/request-handler.d.ts +12 -0
  29. package/dist/core/request-handler.js +322 -0
  30. package/dist/core/router.d.ts +7 -0
  31. package/dist/core/router.js +82 -0
  32. package/dist/core/site-config.d.ts +38 -0
  33. package/dist/core/site-config.js +123 -0
  34. package/dist/html/template-kind.d.ts +1 -0
  35. package/dist/html/template-kind.js +1 -0
  36. package/dist/html/template.d.ts +19 -0
  37. package/dist/html/template.js +67 -0
  38. package/dist/html/theme.d.ts +2 -0
  39. package/dist/html/theme.js +608 -0
  40. package/dist/index-builder.d.ts +13 -0
  41. package/dist/index-builder.js +299 -0
  42. package/package.json +66 -0
@@ -0,0 +1,299 @@
1
+ import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { inferDirectoryContentType } from './core/content-type.js';
4
+ import { getDirectoryIndexCandidates } from './core/directory-index.js';
5
+ import { parseMarkdownDocument } from './core/markdown.js';
6
+ const INDEX_START_MARKER = '<!-- INDEX:START -->';
7
+ const INDEX_END_MARKER = '<!-- INDEX:END -->';
8
+ export async function buildDirectoryIndexes(options) {
9
+ if (!options.rootDir && !options.dir) {
10
+ throw new Error('Expected either rootDir or dir.');
11
+ }
12
+ if (options.rootDir && options.dir) {
13
+ throw new Error('Use either rootDir or dir, not both.');
14
+ }
15
+ if (options.dir) {
16
+ const directoryPath = path.resolve(options.dir);
17
+ const updatedFile = await updateSingleDirectoryIndex(directoryPath, {
18
+ createIfMissing: false,
19
+ });
20
+ return {
21
+ updatedFiles: updatedFile ? [updatedFile] : [],
22
+ skippedDirectories: updatedFile ? [] : [directoryPath],
23
+ };
24
+ }
25
+ const rootDir = path.resolve(options.rootDir);
26
+ const directories = await listDirectoriesRecursively(rootDir);
27
+ const updatedFiles = [];
28
+ const skippedDirectories = [];
29
+ for (const directoryPath of directories) {
30
+ const updatedFile = await updateSingleDirectoryIndex(directoryPath, {
31
+ createIfMissing: false,
32
+ });
33
+ if (updatedFile) {
34
+ updatedFiles.push(updatedFile);
35
+ continue;
36
+ }
37
+ skippedDirectories.push(directoryPath);
38
+ }
39
+ return { updatedFiles, skippedDirectories };
40
+ }
41
+ async function updateSingleDirectoryIndex(directoryPath, options) {
42
+ const indexFilePath = await resolveDirectoryIndexFile(directoryPath);
43
+ if (indexFilePath === null && !options.createIfMissing) {
44
+ return null;
45
+ }
46
+ const targetFilePath = indexFilePath ?? path.join(directoryPath, 'index.md');
47
+ const existingContent = indexFilePath
48
+ ? await readFile(indexFilePath, 'utf8')
49
+ : '';
50
+ const block = await buildManagedIndexBlock(directoryPath);
51
+ const nextContent = upsertManagedIndexBlock(existingContent, block, {
52
+ directoryPath,
53
+ });
54
+ if (nextContent !== existingContent) {
55
+ await writeFile(targetFilePath, nextContent, 'utf8');
56
+ }
57
+ return targetFilePath;
58
+ }
59
+ export async function buildManagedIndexBlock(directoryPath) {
60
+ const entries = await readdir(directoryPath, { withFileTypes: true });
61
+ const directories = [];
62
+ const articles = [];
63
+ for (const entry of entries) {
64
+ if (entry.name.startsWith('.')) {
65
+ continue;
66
+ }
67
+ const fullPath = path.join(directoryPath, entry.name);
68
+ if (entry.isDirectory()) {
69
+ const resolvedEntry = await resolveDirectoryEntry(fullPath, entry.name);
70
+ if (resolvedEntry.draft) {
71
+ continue;
72
+ }
73
+ if (resolvedEntry.type === 'post') {
74
+ articles.push({
75
+ title: resolvedEntry.title,
76
+ date: resolvedEntry.date,
77
+ summary: resolvedEntry.summary,
78
+ link: `./${entry.name}/`,
79
+ order: resolvedEntry.order,
80
+ });
81
+ }
82
+ else {
83
+ directories.push({
84
+ title: resolvedEntry.title,
85
+ link: `./${entry.name}/`,
86
+ order: resolvedEntry.order,
87
+ });
88
+ }
89
+ continue;
90
+ }
91
+ if (!entry.isFile() || path.extname(entry.name).toLowerCase() !== '.md') {
92
+ continue;
93
+ }
94
+ if (entry.name === 'index.md' || entry.name === 'README.md') {
95
+ continue;
96
+ }
97
+ const source = await readFile(fullPath, 'utf8');
98
+ const parsed = await parseMarkdownDocument(entry.name, source);
99
+ if (parsed.meta.draft === true) {
100
+ continue;
101
+ }
102
+ articles.push({
103
+ title: parsed.meta.title ?? entry.name.slice(0, -'.md'.length),
104
+ date: parsed.meta.date,
105
+ summary: parsed.meta.summary ?? extractFirstParagraph(parsed.body),
106
+ link: `./${entry.name}`,
107
+ order: parsed.meta.order,
108
+ });
109
+ }
110
+ directories.sort(compareDirectories);
111
+ articles.sort(compareArticles);
112
+ return renderManagedIndexBlock(directories, articles);
113
+ }
114
+ export function upsertManagedIndexBlock(source, block, options = {}) {
115
+ const hasStart = source.includes(INDEX_START_MARKER);
116
+ const hasEnd = source.includes(INDEX_END_MARKER);
117
+ if (hasStart !== hasEnd) {
118
+ const scope = options.directoryPath ? ` in ${options.directoryPath}` : '';
119
+ throw new Error(`Found only one index marker${scope}.`);
120
+ }
121
+ if (hasStart && hasEnd) {
122
+ const pattern = new RegExp(`${escapeRegExp(INDEX_START_MARKER)}[\\s\\S]*?${escapeRegExp(INDEX_END_MARKER)}`, 'm');
123
+ return source.replace(pattern, block);
124
+ }
125
+ const trimmed = source.trimEnd();
126
+ if (trimmed === '') {
127
+ return `${block}\n`;
128
+ }
129
+ return `${trimmed}\n\n${block}\n`;
130
+ }
131
+ function renderManagedIndexBlock(directories, articles) {
132
+ const lines = [INDEX_START_MARKER, ''];
133
+ if (directories.length > 0) {
134
+ for (const entry of directories) {
135
+ lines.push(`- [${entry.title}](${entry.link})`);
136
+ }
137
+ lines.push('');
138
+ }
139
+ if (articles.length > 0) {
140
+ for (const article of articles) {
141
+ lines.push(`- [${article.title}](${article.link})`);
142
+ const detail = [article.date, article.summary].filter(Boolean).join(' · ');
143
+ if (detail !== '') {
144
+ lines.push(` ${detail}`);
145
+ }
146
+ lines.push('');
147
+ }
148
+ }
149
+ lines.push(INDEX_END_MARKER);
150
+ return lines.join('\n');
151
+ }
152
+ async function resolveDirectoryEntry(directoryPath, fallbackName) {
153
+ const indexPath = await resolveDirectoryIndexFile(directoryPath);
154
+ if (indexPath === null) {
155
+ return {
156
+ title: fallbackName,
157
+ type: 'page',
158
+ draft: false,
159
+ };
160
+ }
161
+ const source = await readFile(indexPath, 'utf8');
162
+ const parsed = await parseMarkdownDocument(path.basename(indexPath), source);
163
+ const shape = await inspectDirectoryShape(directoryPath);
164
+ return {
165
+ title: parsed.meta.title ?? fallbackName,
166
+ type: inferDirectoryContentType(parsed.meta, shape),
167
+ date: parsed.meta.date,
168
+ summary: parsed.meta.summary ?? extractFirstParagraph(parsed.body),
169
+ draft: parsed.meta.draft === true,
170
+ order: parsed.meta.order,
171
+ };
172
+ }
173
+ async function listDirectoriesRecursively(rootDir) {
174
+ const directories = [rootDir];
175
+ const entries = await readdir(rootDir, { withFileTypes: true });
176
+ for (const entry of entries) {
177
+ if (!entry.isDirectory() || entry.name.startsWith('.')) {
178
+ continue;
179
+ }
180
+ directories.push(...(await listDirectoriesRecursively(path.join(rootDir, entry.name))));
181
+ }
182
+ return directories;
183
+ }
184
+ async function inspectDirectoryShape(directoryPath) {
185
+ const entries = await readdir(directoryPath, { withFileTypes: true });
186
+ let hasChildDirectories = false;
187
+ let hasExtraMarkdownFiles = false;
188
+ let hasAssetFiles = false;
189
+ for (const entry of entries) {
190
+ if (entry.name.startsWith('.')) {
191
+ continue;
192
+ }
193
+ if (entry.isDirectory()) {
194
+ hasChildDirectories = true;
195
+ continue;
196
+ }
197
+ if (!entry.isFile()) {
198
+ continue;
199
+ }
200
+ const extension = path.extname(entry.name).toLowerCase();
201
+ if (extension === '.md') {
202
+ if (entry.name !== 'index.md' && entry.name !== 'README.md') {
203
+ hasExtraMarkdownFiles = true;
204
+ }
205
+ continue;
206
+ }
207
+ hasAssetFiles = true;
208
+ }
209
+ return {
210
+ hasChildDirectories,
211
+ hasExtraMarkdownFiles,
212
+ hasAssetFiles,
213
+ };
214
+ }
215
+ function compareDirectories(left, right) {
216
+ const orderComparison = compareOptionalOrder(left.order, right.order);
217
+ if (orderComparison !== 0) {
218
+ return orderComparison;
219
+ }
220
+ return left.title.localeCompare(right.title);
221
+ }
222
+ function compareArticles(left, right) {
223
+ const orderComparison = compareOptionalOrder(left.order, right.order);
224
+ if (orderComparison !== 0) {
225
+ return orderComparison;
226
+ }
227
+ const leftTimestamp = left.date ? Date.parse(left.date) : Number.NaN;
228
+ const rightTimestamp = right.date ? Date.parse(right.date) : Number.NaN;
229
+ if (!Number.isNaN(leftTimestamp) && !Number.isNaN(rightTimestamp)) {
230
+ if (leftTimestamp !== rightTimestamp) {
231
+ return rightTimestamp - leftTimestamp;
232
+ }
233
+ }
234
+ else if (!Number.isNaN(leftTimestamp)) {
235
+ return -1;
236
+ }
237
+ else if (!Number.isNaN(rightTimestamp)) {
238
+ return 1;
239
+ }
240
+ return left.title.localeCompare(right.title);
241
+ }
242
+ function compareOptionalOrder(leftOrder, rightOrder) {
243
+ if (leftOrder !== undefined && rightOrder !== undefined) {
244
+ if (leftOrder !== rightOrder) {
245
+ return leftOrder - rightOrder;
246
+ }
247
+ return 0;
248
+ }
249
+ if (leftOrder !== undefined) {
250
+ return -1;
251
+ }
252
+ if (rightOrder !== undefined) {
253
+ return 1;
254
+ }
255
+ return 0;
256
+ }
257
+ function extractFirstParagraph(markdown) {
258
+ const paragraphs = markdown
259
+ .split(/\n\s*\n/g)
260
+ .map((paragraph) => paragraph.trim())
261
+ .filter((paragraph) => paragraph !== '');
262
+ for (const paragraph of paragraphs) {
263
+ if (paragraph.startsWith('#') ||
264
+ paragraph.startsWith('<!--') ||
265
+ paragraph.startsWith('- ') ||
266
+ paragraph.startsWith('* ')) {
267
+ continue;
268
+ }
269
+ return paragraph.replace(/\s+/g, ' ');
270
+ }
271
+ return undefined;
272
+ }
273
+ async function resolveDirectoryIndexFile(directoryPath) {
274
+ for (const candidate of getDirectoryIndexCandidates('')) {
275
+ const candidatePath = path.join(directoryPath, candidate);
276
+ if (await pathExists(candidatePath)) {
277
+ return candidatePath;
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+ async function pathExists(filePath) {
283
+ try {
284
+ await stat(filePath);
285
+ return true;
286
+ }
287
+ catch (error) {
288
+ if (typeof error === 'object' &&
289
+ error !== null &&
290
+ 'code' in error &&
291
+ error.code === 'ENOENT') {
292
+ return false;
293
+ }
294
+ throw error;
295
+ }
296
+ }
297
+ function escapeRegExp(value) {
298
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
299
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "mdorigin",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Markdown-first publishing engine with raw Markdown and HTML views.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jolestar/mdorigin.git"
9
+ },
10
+ "homepage": "https://github.com/jolestar/mdorigin#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/jolestar/mdorigin/issues"
13
+ },
14
+ "keywords": [
15
+ "markdown",
16
+ "publishing",
17
+ "static-site",
18
+ "cloudflare-workers",
19
+ "cli"
20
+ ],
21
+ "bin": {
22
+ "mdorigin": "./dist/cli/main.js"
23
+ },
24
+ "exports": {
25
+ "./cloudflare": {
26
+ "types": "./dist/cloudflare.d.ts",
27
+ "default": "./dist/cloudflare.js"
28
+ },
29
+ "./cloudflare-runtime": {
30
+ "types": "./dist/cloudflare-runtime.d.ts",
31
+ "default": "./dist/cloudflare-runtime.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist/",
36
+ "README.md"
37
+ ],
38
+ "scripts": {
39
+ "dev": "tsx src/cli/main.ts dev",
40
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
41
+ "build:index": "tsx src/cli/main.ts build index",
42
+ "build:cloudflare": "tsx src/cli/main.ts build cloudflare",
43
+ "init:cloudflare": "tsx src/cli/main.ts init cloudflare",
44
+ "check": "tsc --noEmit -p tsconfig.json",
45
+ "test": "node --test --import tsx src/*.test.ts src/**/*.test.ts",
46
+ "prepack": "npm run build",
47
+ "pack:check": "npm pack --dry-run"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "engines": {
53
+ "node": ">=20"
54
+ },
55
+ "dependencies": {
56
+ "gray-matter": "^4.0.3",
57
+ "remark": "^15.0.1",
58
+ "remark-gfm": "^4.0.1",
59
+ "remark-html": "^16.0.1"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^24.5.2",
63
+ "tsx": "^4.20.5",
64
+ "typescript": "^5.9.2"
65
+ }
66
+ }