You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
4.1 KiB

import { readFile, writeFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { Command } from 'commander';
import open from 'open';
import updateNotifier from 'update-notifier';
import { readPackageUp } from 'read-pkg-up';
import { CSSItem, JSItem, buildJSItem } from 'markmap-common';
import {
Transformer,
baseJsPaths,
type IMarkmapCreateOptions,
type IAssets,
} from 'markmap-lib';
import {
IDevelopOptions,
addToolbar,
localProvider,
resolveFile,
} from './util';
import { develop } from './dev-server';
export * from 'markmap-lib';
export { develop };
async function loadFile(path: string) {
if (path.startsWith('/node_modules/')) {
const relpath = path.slice(14);
return readFile(await resolveFile(relpath), 'utf8');
}
const res = await fetch(path);
if (!res.ok) throw res;
return res.text();
}
async function inlineAssets(assets: IAssets): Promise<IAssets> {
const [scripts, styles] = await Promise.all([
Promise.all(
(assets.scripts || []).map(
async (item): Promise<JSItem> =>
item.type === 'script' && item.data.src
? {
type: 'script',
data: {
textContent: await loadFile(item.data.src),
},
}
: item
)
),
Promise.all(
(assets.styles || []).map(
async (item): Promise<CSSItem> =>
item.type === 'stylesheet'
? {
type: 'style',
data: await loadFile(item.data.href),
}
: item
)
),
]);
return {
scripts,
styles,
};
}
export async function createMarkmap(
options: IMarkmapCreateOptions & IDevelopOptions
): Promise<void> {
const transformer = new Transformer();
if (options.offline) {
transformer.urlBuilder.setProvider('local', localProvider);
transformer.urlBuilder.provider = 'local';
} else {
await transformer.urlBuilder.findFastestProvider();
}
const { root, features, frontmatter } = transformer.transform(
options.content || ''
);
let assets = transformer.getUsedAssets(features);
assets = {
...assets,
scripts: [
...baseJsPaths
.map((path) => transformer.urlBuilder.getFullUrl(path))
.map((path) => buildJSItem(path)),
...(assets.scripts || []),
],
};
if (options.toolbar) {
assets = addToolbar(transformer, assets);
}
if (options.offline) {
assets = await inlineAssets(assets);
}
const html = transformer.fillTemplate(root, assets, {
baseJs: [],
jsonOptions: (frontmatter as any)?.markmap,
});
const output = options.output || 'markmap.html';
await writeFile(output, html, 'utf8');
if (options.open) open(output);
}
export async function main() {
const pkg = (
await readPackageUp({
cwd: fileURLToPath(import.meta.url),
})
)?.packageJson;
if (!pkg) throw new Error('package.json not found');
const notifier = updateNotifier({ pkg });
notifier.notify();
const program = new Command();
program
.version(pkg.version)
.description('Create a markmap from a Markdown input file')
.arguments('<input>')
.option('--no-open', 'do not open the output file after generation')
.option('--no-toolbar', 'do not show toolbar')
.option('-o, --output <output>', 'specify filename of the output HTML')
.option(
'--offline',
'Inline all assets to allow the generated HTML to work offline'
)
.option(
'-w, --watch',
'watch the input file and update output on the fly, note that this feature is for development only'
)
.action(async (input, cmd) => {
const content = await readFile(input, 'utf8');
const output = cmd.output || `${input.replace(/\.\w*$/, '')}.html`;
if (cmd.watch) {
await develop(input, {
open: cmd.open,
toolbar: cmd.toolbar,
offline: true,
});
} else {
await createMarkmap({
content,
output,
open: cmd.open,
toolbar: cmd.toolbar,
offline: cmd.offline,
});
}
});
program.parse(process.argv);
}