UNPKG

markmap-cli

Version:
431 lines (430 loc) 12.2 kB
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 };