mdzilla 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/_chunks/exporter.mjs +48 -4
- package/dist/_chunks/server.mjs +497 -322
- package/dist/index.d.mts +18 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -67,6 +67,19 @@ npx mdzilla gh:unjs/h3 /guide/basics # Render a specific page
|
|
|
67
67
|
npx mdzilla gh:unjs/h3 router # Search for 'router'
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
### Web Server
|
|
71
|
+
|
|
72
|
+
Running `mdzilla <source>` without a query opens docs in the browser with a local web server:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
npx mdzilla ./docs # Browse local docs in browser
|
|
76
|
+
npx mdzilla gh:unjs/h3 # Browse GitHub repo docs
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The web UI provides a sidebar navigation, full-text search, syntax-highlighted pages, and dark/light theme support.
|
|
80
|
+
|
|
81
|
+
For local sources (`FSSource`), the server watches for file changes and live-reloads both the navigation and the current page via Server-Sent Events — no manual refresh needed.
|
|
82
|
+
|
|
70
83
|
### Plain Mode
|
|
71
84
|
|
|
72
85
|
Use `--plain` for plain text output. Auto-enabled when piping output or when called by AI agents.
|
|
@@ -123,6 +136,13 @@ const results = docs.filter("instal"); // sorted by match score
|
|
|
123
136
|
|
|
124
137
|
// Substring match (returns indices into docs.flat)
|
|
125
138
|
const indices = docs.matchIndices("getting started");
|
|
139
|
+
|
|
140
|
+
// Watch for changes (FSSource only) with live reload
|
|
141
|
+
docs.watch();
|
|
142
|
+
const unsub = docs.onChange((path) => {
|
|
143
|
+
console.log(`Changed: ${path}`);
|
|
144
|
+
});
|
|
145
|
+
// Later: unsub() and docs.unwatch()
|
|
126
146
|
```
|
|
127
147
|
|
|
128
148
|
`resolveSource` auto-detects the source type from the input string (`gh:`, `npm:`, `https://`, or local path). You can also use specific source classes directly (`FSSource`, `GitSource`, `NpmSource`, `HTTPSource`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseMeta, renderToMarkdown, renderToText } from "md4x";
|
|
2
2
|
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync, watch } from "node:fs";
|
|
3
4
|
import { basename, dirname, extname, join } from "node:path";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
//#region src/collection.ts
|
|
7
7
|
var Collection = class {
|
|
@@ -10,6 +10,8 @@ var Collection = class {
|
|
|
10
10
|
flat = [];
|
|
11
11
|
_fileMap = /* @__PURE__ */ new Map();
|
|
12
12
|
_contentCache = /* @__PURE__ */ new Map();
|
|
13
|
+
_changeListeners = /* @__PURE__ */ new Set();
|
|
14
|
+
_reloadTimer;
|
|
13
15
|
constructor(source) {
|
|
14
16
|
this.source = source;
|
|
15
17
|
}
|
|
@@ -23,6 +25,28 @@ var Collection = class {
|
|
|
23
25
|
async reload() {
|
|
24
26
|
return this.load();
|
|
25
27
|
}
|
|
28
|
+
/** Start watching source for changes. Debounces, reloads collection, and notifies listeners. */
|
|
29
|
+
watch() {
|
|
30
|
+
this.source.watch(({ path }) => {
|
|
31
|
+
clearTimeout(this._reloadTimer);
|
|
32
|
+
this._reloadTimer = setTimeout(() => {
|
|
33
|
+
this.reload().then(() => {
|
|
34
|
+
for (const listener of this._changeListeners) listener(path);
|
|
35
|
+
});
|
|
36
|
+
}, 100);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Stop watching source. */
|
|
40
|
+
unwatch() {
|
|
41
|
+
clearTimeout(this._reloadTimer);
|
|
42
|
+
this.source.unwatch();
|
|
43
|
+
this._changeListeners.clear();
|
|
44
|
+
}
|
|
45
|
+
/** Register a change listener. Returns unsubscribe function. */
|
|
46
|
+
onChange(listener) {
|
|
47
|
+
this._changeListeners.add(listener);
|
|
48
|
+
return () => this._changeListeners.delete(listener);
|
|
49
|
+
}
|
|
26
50
|
/** Get raw file content for a flat entry (cached). */
|
|
27
51
|
async getContent(entry) {
|
|
28
52
|
if (!entry.filePath || entry.entry.page === false) return void 0;
|
|
@@ -354,11 +378,13 @@ async function _scanNav(dirPath, parentPath, options) {
|
|
|
354
378
|
} else if (extname(entry) === ".md") {
|
|
355
379
|
const { order, slug, draft } = parseNumberedName(basename(entry));
|
|
356
380
|
if (draft && !options.drafts) continue;
|
|
357
|
-
const
|
|
381
|
+
const rawMeta = parseMeta(await readFile(fullPath, "utf8"));
|
|
382
|
+
const meta = applyNavigationOverride(rawMeta);
|
|
358
383
|
if (meta.navigation === false) continue;
|
|
359
384
|
const resolvedOrder = typeof meta.order === "number" ? meta.order : order;
|
|
360
385
|
const resolvedSlug = slug === "index" ? "" : slug;
|
|
361
|
-
const
|
|
386
|
+
const firstHeading = rawMeta.headings?.find((h) => h.level === 1)?.text;
|
|
387
|
+
const title = meta.title || firstHeading || humanizeSlug(slug) || "index";
|
|
362
388
|
const entryPath = resolvedSlug === "" ? parentPath === "/" ? "/" : parentPath : parentPath === "/" ? `/${resolvedSlug}` : `${parentPath}/${resolvedSlug}`;
|
|
363
389
|
const extra = extraMeta(meta);
|
|
364
390
|
const navEntry = {
|
|
@@ -366,6 +392,7 @@ async function _scanNav(dirPath, parentPath, options) {
|
|
|
366
392
|
path: entryPath,
|
|
367
393
|
title,
|
|
368
394
|
order: resolvedOrder,
|
|
395
|
+
...firstHeading && firstHeading !== title ? { heading: firstHeading } : {},
|
|
369
396
|
...meta.icon ? { icon: meta.icon } : {},
|
|
370
397
|
...meta.description ? { description: meta.description } : {},
|
|
371
398
|
...draft ? { draft: true } : {},
|
|
@@ -379,11 +406,17 @@ async function _scanNav(dirPath, parentPath, options) {
|
|
|
379
406
|
}
|
|
380
407
|
//#endregion
|
|
381
408
|
//#region src/sources/_base.ts
|
|
382
|
-
var Source = class {
|
|
409
|
+
var Source = class {
|
|
410
|
+
/** Start watching for file changes. Override in sources that support it. */
|
|
411
|
+
watch(_callback) {}
|
|
412
|
+
/** Stop watching. Override in sources that support it. */
|
|
413
|
+
unwatch() {}
|
|
414
|
+
};
|
|
383
415
|
//#endregion
|
|
384
416
|
//#region src/sources/fs.ts
|
|
385
417
|
var FSSource = class extends Source {
|
|
386
418
|
dir;
|
|
419
|
+
_watcher;
|
|
387
420
|
constructor(dir) {
|
|
388
421
|
super();
|
|
389
422
|
this.dir = dir;
|
|
@@ -399,6 +432,17 @@ var FSSource = class extends Source {
|
|
|
399
432
|
async readContent(filePath) {
|
|
400
433
|
return readFile(filePath, "utf8");
|
|
401
434
|
}
|
|
435
|
+
watch(callback) {
|
|
436
|
+
this.unwatch();
|
|
437
|
+
this._watcher = watch(this.dir, { recursive: true }, (_event, filename) => {
|
|
438
|
+
if (!filename || filename.startsWith(".") || filename.startsWith("_")) return;
|
|
439
|
+
callback({ path: "/" + filename });
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
unwatch() {
|
|
443
|
+
this._watcher?.close();
|
|
444
|
+
this._watcher = void 0;
|
|
445
|
+
}
|
|
402
446
|
};
|
|
403
447
|
function parseSlug(name) {
|
|
404
448
|
const base = name.endsWith(".draft") ? name.slice(0, -6) : name;
|