hackmd-to-html-cli
Version:
A node.js CLI tool for converting HackMD markdown to HTML.
236 lines • 10.2 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = __importDefault(require("commander"));
const fs_1 = __importDefault(require("fs"));
const converter_1 = require("./converter");
const path_1 = __importDefault(require("path"));
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const glob_1 = require("glob");
const node_crypto_1 = require("node:crypto");
const utils_1 = require("./markdown/utils");
const package_json_1 = require("../package.json");
const hash = (0, node_crypto_1.createHash)('sha256');
commander_1.default.program.version(package_json_1.version, '-v, --version', 'output the current version');
commander_1.default.program
.requiredOption('-i, --input <files_or_urls...>', 'the path/url of input markdown files')
.addOption(new commander_1.default.Option('-d, --dest <dir>', 'the path of output directory (filename is generated automatically)').default('', './output'))
.addOption(new commander_1.default.Option('-o, --output <files...>', 'the path of output file (ignored if the flag -d is set)').default('', '""'))
.addOption(new commander_1.default.Option('-l, --layout <html_file>', 'specify the layout file').default('', '""'))
.addOption(new commander_1.default.Option('-b, --hardBreak', 'use hard break instead of soft break'))
.addOption(new commander_1.default.Option('-k, --dark', 'use the dark mode layout (activate only if the -l option is not set)'))
.parse(process.argv);
const options = commander_1.default.program.opts();
const inputs = options.input;
const dest = options.dest === '' ? './output' : options.dest;
const outputs = options.dest === '' && options.output !== '' ? options.output : null;
const inputLayout = options.layout !== '' ? fs_1.default.readFileSync(options.layout, { encoding: 'utf-8' }) : null;
const hardBreak = options.hardBreak;
const darkMode = options.dark;
function main() {
const converter = new converter_1.Converter({
html: true,
breaks: !hardBreak,
linkify: true,
typographer: true
});
let errorCounter = 0;
let outputsIndex = 0;
const outputFilenameSet = new Set();
// load layout
const layout = inputLayout !== null && inputLayout !== void 0 ? inputLayout : defaultLayout(darkMode);
const isURL = (s) => {
try {
const url = new URL(s);
return url;
}
catch (_a) {
return null;
}
};
const printError = (fn, e) => {
console.error(`❌ #${errorCounter} ${fn}`);
console.error(`${e}`);
errorCounter++;
};
const convertURL = (inputURL, output, res) => {
let data = "";
res.on('data', (d) => {
data += d;
});
res.on('end', () => {
const res = converter.render(data);
const converted = renderToLayout(res, layout);
try {
fs_1.default.writeFileSync(output, converted);
console.log(`✅ ${inputURL} ➡️ ${output}`);
}
catch (e) {
printError(inputURL, e);
return;
}
});
};
const generateOutputFilename = (inputFilename) => {
// if `outputs` is non-null, use `outputs` as output file name
let ret;
if (outputs !== null) {
if (outputsIndex < outputs.length) {
ret = outputs[outputsIndex].toString();
outputsIndex++;
}
else {
throw ('the number of --output is smaller than the number of --input');
}
}
else {
ret = path_1.default.join(dest.toString(), path_1.default.basename(inputFilename.toString()).replace(/\.md$/, '') + '.html');
}
// if `ret` is repeated, use a hash function to generate a new filename
let hashIn = inputFilename.toString();
const retExtname = path_1.default.extname(ret); // including leading dot '.'
const tmpRet = ret.replace(new RegExp(retExtname + "$"), '');
while (outputFilenameSet.has(ret)) {
hashIn = hash.update(hashIn).digest('hex').toString().substring(0, 5);
ret = tmpRet + '.' + hashIn + retExtname;
}
outputFilenameSet.add(ret);
return ret;
};
// if `outputs` is null, generate the output file in the directory `dest`
// if `dest` existed, check `dest` is directory
// otherwise create the new directory `dest`
if (outputs === null) {
if (fs_1.default.existsSync(dest)) {
const stats = fs_1.default.statSync(dest);
if (!stats.isDirectory()) {
printError(dest, `${dest} is not directory`);
return;
}
}
if (!fs_1.default.existsSync(dest)) {
fs_1.default.mkdirSync(dest);
}
}
inputs.forEach((fn) => {
// 1. http/https mode
const url = isURL(fn.toString());
if (url != null) {
if (outputs !== null && outputsIndex >= (outputs === null || outputs === void 0 ? void 0 : outputs.length)) {
return;
}
if (url.protocol === 'https:') {
https.get(url, (res) => {
convertURL(url, generateOutputFilename(url), res);
}).on('error', (e) => {
printError(fn, e);
});
}
else if (url.protocol === 'http:') {
http.get(url, (res) => {
convertURL(url, generateOutputFilename(url), res);
}).on('error', (e) => {
printError(fn, e);
});
}
else {
printError(url, "protocol not supported");
}
}
else {
// 2. File mode
(0, glob_1.glob)(fn.toString()).then((fileList) => {
fileList.forEach((f) => {
try {
const stats = fs_1.default.statSync(f);
if (stats.isDirectory()) {
return;
}
}
catch (e) {
printError(fn, e);
return;
}
const markdown = fs_1.default.readFileSync(f, { encoding: 'utf-8' });
const res = converter.render(markdown);
const converted = renderToLayout(res, layout);
const o = generateOutputFilename(f);
try {
fs_1.default.writeFileSync(o, converted);
console.log(`✅ ${f} ➡️ ${o}`);
}
catch (e) {
printError(f, e);
}
});
}).catch((e) => {
printError(fn, e);
});
}
});
}
function renderToLayout(res, layout) {
let metas = '';
if (res.metadata.title !== '') {
metas += '<title>' + (0, utils_1.escapeHtml)(res.metadata.title) + '</title>\n';
metas += '<meta name="twitter:title" content="' + (0, utils_1.escapeHtml)(res.metadata.title) + '" />\n';
metas += '<meta property="og:title" content="' + (0, utils_1.escapeHtml)(res.metadata.title) + '" />\n';
}
if (res.metadata.robots !== '') {
metas += '<meta name="robots" content="' + (0, utils_1.escapeHtml)(res.metadata.robots) + '">\n';
}
if (res.metadata.description !== '') {
metas += '<meta name="description" content="' + (0, utils_1.escapeHtml)(res.metadata.description) + '">\n';
metas += '<meta name="twitter:description" content="' + (0, utils_1.escapeHtml)(res.metadata.description) + '">\n';
metas += '<meta property="og:description" content="' + (0, utils_1.escapeHtml)(res.metadata.description) + '">\n';
}
if (res.metadata.image !== '') {
metas += '<meta name="twitter:image:src" content="' + (0, utils_1.escapeHtml)(res.metadata.image) + '" />\n';
metas += '<meta property="og:image" content="' + (0, utils_1.escapeHtml)(res.metadata.image) + '" />\n';
}
let lang = '';
if (res.metadata.lang !== '') {
lang = ' lang="' + (0, utils_1.escapeHtml)(res.metadata.lang) + '"';
}
let dir = '';
if (res.metadata.dir !== '') {
dir = ' dir="' + (0, utils_1.escapeHtml)(res.metadata.dir) + '"';
}
return layout
.replace('{{lang}}', lang)
.replace('{{dir}}', dir)
.replace('{{metas}}', metas)
.replace('{{main}}', res.main);
}
function defaultLayout(dark = false) {
return fs_1.default.readFileSync(path_1.default.join(__dirname, !dark ? '../layouts/layout.html' : '../layouts/layout.dark.html'), { encoding: 'utf-8' });
}
main();
//# sourceMappingURL=cli.js.map
;