markmap-cli
Version:
Create markmaps from CLI
431 lines (430 loc) • 12.2 kB
JavaScript
import { readFile, stat, writeFile } from "fs/promises";
import { mergeAssets, defer, buildJSItem } from "markmap-common";
import { Transformer } from "markmap-lib";
import * as markmapLib from "markmap-lib";
import { fillTemplate, baseJsPaths } from "markmap-render";
import open from "open";
import { join, resolve } from "path";
import { t as toolbarAssets, A as ASSETS_PREFIX, l as localProvider, c as createStreamBody, a as config } from "./common-DBhJ53_3.js";
import { serve } from "@hono/node-server";
import { watch } from "chokidar";
import { createHash } from "crypto";
import { createReadStream } from "fs";
import { Hono } from "hono";
import { getPortPromise } from "portfinder";
var getMimeType = (filename, mimes = baseMimes) => {
const regexp = /\.([a-zA-Z0-9]+?)$/;
const match = filename.match(regexp);
if (!match) {
return;
}
let mimeType = mimes[match[1]];
if (mimeType && mimeType.startsWith("text")) {
mimeType += "; charset=utf-8";
}
return mimeType;
};
var _baseMimes = {
aac: "audio/aac",
avi: "video/x-msvideo",
avif: "image/avif",
av1: "video/av1",
bin: "application/octet-stream",
bmp: "image/bmp",
css: "text/css",
csv: "text/csv",
eot: "application/vnd.ms-fontobject",
epub: "application/epub+zip",
gif: "image/gif",
gz: "application/gzip",
htm: "text/html",
html: "text/html",
ico: "image/x-icon",
ics: "text/calendar",
jpeg: "image/jpeg",
jpg: "image/jpeg",
js: "text/javascript",
json: "application/json",
jsonld: "application/ld+json",
map: "application/json",
mid: "audio/x-midi",
midi: "audio/x-midi",
mjs: "text/javascript",
mp3: "audio/mpeg",
mp4: "video/mp4",
mpeg: "video/mpeg",
oga: "audio/ogg",
ogv: "video/ogg",
ogx: "application/ogg",
opus: "audio/opus",
otf: "font/otf",
pdf: "application/pdf",
png: "image/png",
rtf: "application/rtf",
svg: "image/svg+xml",
tif: "image/tiff",
tiff: "image/tiff",
ts: "video/mp2t",
ttf: "font/ttf",
txt: "text/plain",
wasm: "application/wasm",
webm: "video/webm",
weba: "audio/webm",
webp: "image/webp",
woff: "font/woff",
woff2: "font/woff2",
xhtml: "application/xhtml+xml",
xml: "application/xml",
zip: "application/zip",
"3gp": "video/3gpp",
"3g2": "video/3gpp2",
gltf: "model/gltf+json",
glb: "model/gltf-binary"
};
var baseMimes = _baseMimes;
function sequence(fn) {
let promise;
return () => {
promise || (promise = Promise.resolve(fn()).finally(() => {
promise = void 0;
}));
return promise;
};
}
class BufferContentProvider {
constructor(key) {
this.key = key;
this.deferredSet = /* @__PURE__ */ new Set();
this.state = {
content: {
ts: 0,
value: ""
},
line: {
ts: 0,
value: 0
}
};
this.disposeList = [];
}
async getUpdate(query, timeout = 1e4) {
const deferred = defer();
this.deferredSet.add(deferred);
setTimeout(() => {
this.feed(null, deferred);
}, timeout);
if (Object.keys(query).some((key) => query[key] < this.state[key].ts)) {
this.feed(null, deferred);
}
await deferred.promise;
}
feed(data, deferred) {
if (data) {
Object.assign(this.state, data);
}
if (deferred) {
deferred.resolve();
this.deferredSet.delete(deferred);
} else {
for (const d of this.deferredSet) {
d.resolve();
}
this.deferredSet.clear();
}
}
setCursor(line) {
this.feed({
line: {
ts: Date.now(),
value: line
}
});
}
setContent(content) {
this.feed({
content: {
ts: Date.now(),
value: content
}
});
}
dispose() {
this.disposeList.forEach((dispose) => dispose());
}
}
class FileSystemProvider extends BufferContentProvider {
constructor(key, filePath, watch2) {
super(key);
this.filePath = filePath;
this.disposeList.push(watch2(() => this.update()));
}
async update() {
const content = await readFile(this.filePath, "utf8");
this.setContent(content);
}
}
function sha256(input) {
return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 7);
}
async function sendStatic(c, realpath) {
try {
const result = await stat(realpath);
if (!result.isFile()) throw new Error("File not found");
} catch {
return c.body("File not found", 404);
}
const stream = createReadStream(realpath);
const type = getMimeType(realpath);
if (type) c.header("content-type", type);
return c.body(createStreamBody(stream));
}
class MarkmapDevServer {
constructor(options, transformer) {
this.options = options;
this.providers = {};
this.callbacks = {};
this.disposeList = [];
this.serverInfo = null;
this.transformer = transformer || new Transformer();
this.html = this._buildHtml();
this.disposeList.push(() => {
var _a;
(_a = this.watcher) == null ? void 0 : _a.close();
});
}
_buildHtml() {
var _a, _b;
const otherAssets = mergeAssets(
this.options.toolbar ? toolbarAssets : null,
{
scripts: [
{
type: "iife",
data: {
fn: (options) => {
window.markmap.cliOptions = options;
},
getParams: () => [this.options]
}
}
]
}
);
const assets = mergeAssets(this.transformer.getAssets(), {
scripts: (_a = otherAssets.scripts) == null ? void 0 : _a.map(
(item) => this.transformer.resolveJS(item)
),
styles: (_b = otherAssets.styles) == null ? void 0 : _b.map(
(item) => this.transformer.resolveCSS(item)
)
});
const html = fillTemplate(null, assets, {
urlBuilder: this.transformer.urlBuilder
}) + '<script src="/~client.js"><\/script>';
return html;
}
async setup() {
if (this.serverInfo) throw new Error("Server already set up");
const app = new Hono();
app.get("/", (c) => {
const key = c.req.query("key") || "";
if (!this.providers[key]) return c.notFound();
return c.html(this.html);
});
app.get("/~data", async (c) => {
const key = c.req.query("key") || "";
const provider = this.providers[key];
if (!provider) return c.json({}, 404);
const query = Object.fromEntries(
["content", "line"].map((key2) => [key2, +(c.req.query(key2) || "") || 0])
);
await provider.getUpdate(query);
const updatedKeys = Object.keys(query).filter(
(key2) => query[key2] < provider.state[key2].ts
);
const result = Object.fromEntries(
updatedKeys.map((key2) => {
let data = provider.state[key2];
if (key2 === "content") {
const result2 = this.transformer.transform(data.value);
data = {
...data,
value: {
frontmatter: result2.frontmatter,
root: result2.root
}
};
}
return [key2, data];
})
);
return c.json(result);
});
app.post("/~api", async (c) => {
var _a;
const key = c.req.query("key") || "";
const provider = this.providers[key];
if (!provider) return c.json({}, 404);
const { cmd, args } = await c.req.json();
await ((_a = provider[cmd]) == null ? void 0 : _a.call(provider, ...args));
return c.body(null, 204);
});
const { distDir, assetsDir } = config;
app.get("/~client.*", async (c) => {
const realpath = join(distDir, c.req.path.slice(2));
return sendStatic(c, realpath);
});
app.get(`${ASSETS_PREFIX}*`, async (c) => {
const relpath = c.req.path.slice(ASSETS_PREFIX.length);
const realpath = join(assetsDir, relpath);
return sendStatic(c, realpath);
});
const deferred = defer();
const server = serve(
{
fetch: app.fetch,
port: this.options.port || await getPortPromise()
},
deferred.resolve
);
const address = await deferred.promise;
this.serverInfo = {
server,
address
};
}
async shutdown() {
if (!this.serverInfo) throw new Error("Server is not set up yet");
const deferred = defer();
this.serverInfo.server.close((err) => {
if (err) deferred.reject();
else deferred.resolve();
});
await deferred.promise;
this.serverInfo = null;
}
async destroy() {
await this.shutdown();
this.disposeList.forEach((dispose) => dispose());
}
_watch(filePath, callback) {
let { watcher } = this;
if (!watcher) {
watcher = watch([]).on("all", (_event, path) => {
const callback2 = this.callbacks[path];
callback2 == null ? void 0 : callback2();
});
this.watcher = watcher;
}
watcher.add(filePath);
this.callbacks[filePath] = sequence(callback);
return () => {
watcher.unwatch(filePath);
delete this.callbacks[filePath];
};
}
addProvider(options) {
var _a;
const filePath = (options == null ? void 0 : options.filePath) && resolve(options.filePath);
const key = (options == null ? void 0 : options.key) || (filePath ? sha256(filePath) : Math.random().toString(36).slice(2, 9));
(_a = this.providers)[key] || (_a[key] = filePath ? new FileSystemProvider(
key,
filePath,
(callback) => this._watch(filePath, callback)
) : new BufferContentProvider(key));
return this.providers[key];
}
delProvider(key) {
const provider = this.providers[key];
provider == null ? void 0 : provider.dispose();
delete this.providers[key];
}
}
async function develop(options) {
const transformer = new Transformer();
transformer.urlBuilder.setProvider("local", localProvider);
transformer.urlBuilder.provider = "local";
const devServer = new MarkmapDevServer(options, transformer);
await devServer.setup();
return devServer;
}
async function loadFile(path) {
if (path.startsWith(ASSETS_PREFIX)) {
const relpath = path.slice(ASSETS_PREFIX.length);
return readFile(resolve(config.assetsDir, relpath), "utf8");
}
const res = await fetch(path);
if (!res.ok) throw res;
return res.text();
}
async function inlineAssets(assets) {
const [scripts, styles] = await Promise.all([
Promise.all(
(assets.scripts || []).map(
async (item) => item.type === "script" && item.data.src ? {
type: "script",
data: {
textContent: await loadFile(item.data.src)
}
} : item
)
),
Promise.all(
(assets.styles || []).map(
async (item) => item.type === "stylesheet" ? {
type: "style",
data: await loadFile(item.data.href)
} : item
)
)
]);
return {
scripts,
styles
};
}
async function createMarkmap(options) {
var _a, _b;
const transformer = new Transformer();
if (options.offline) {
transformer.urlBuilder.setProvider("local", localProvider);
transformer.urlBuilder.provider = "local";
} else {
try {
await transformer.urlBuilder.findFastestProvider();
} catch {
}
}
const { root, features, frontmatter } = transformer.transform(
options.content || ""
);
const otherAssets = mergeAssets(
{
scripts: baseJsPaths.map(buildJSItem)
},
options.toolbar ? toolbarAssets : null
);
let assets = mergeAssets(
{
scripts: (_a = otherAssets.scripts) == null ? void 0 : _a.map((item) => transformer.resolveJS(item)),
styles: (_b = otherAssets.styles) == null ? void 0 : _b.map((item) => transformer.resolveCSS(item))
},
transformer.getUsedAssets(features)
);
if (options.offline) assets = await inlineAssets(assets);
const html = fillTemplate(root, assets, {
baseJs: [],
jsonOptions: frontmatter == null ? void 0 : frontmatter.markmap,
urlBuilder: transformer.urlBuilder
});
const output = options.output || "markmap.html";
await writeFile(output, html, "utf8");
if (options.open) open(output);
}
export {
MarkmapDevServer,
config,
createMarkmap,
develop,
markmapLib as markmap
};