diff --git a/bin/main.js b/bin/main.js new file mode 100644 index 0000000000..2ffa88137c --- /dev/null +++ b/bin/main.js @@ -0,0 +1,270 @@ +#!/usr/bin/env node + +/** + * Marked CLI + * Copyright (c) 2011-2013, Christopher Jeffrey (MIT License) + */ + +import { promises } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { homedir } from 'node:os'; +import { createRequire } from 'node:module'; +import { marked } from '../lib/marked.esm.js'; + +const { access, readFile, writeFile } = promises; +const require = createRequire(import.meta.url); + +/** + * @param {Process} nodeProcess inject process so it can be mocked in tests. + */ +export async function main(nodeProcess) { + /** + * Man Page + */ + async function help() { + const { spawn } = await import('child_process'); + const { fileURLToPath } = await import('url'); + + const options = { + cwd: nodeProcess.cwd(), + env: nodeProcess.env, + stdio: 'inherit' + }; + + const __dirname = dirname(fileURLToPath(import.meta.url)); + const helpText = await readFile(resolve(__dirname, '../man/marked.1.txt'), 'utf8'); + + // eslint-disable-next-line promise/param-names + await new Promise(res => { + spawn('man', [resolve(__dirname, '../man/marked.1')], options) + .on('error', () => { + console.log(helpText); + }) + .on('close', res); + }); + } + + async function version() { + const pkg = require('../package.json'); + console.log(pkg.version); + } + + /** + * Main + */ + async function start(argv) { + const files = []; + const options = {}; + let input; + let output; + let string; + let arg; + let tokens; + let config; + let opt; + + function getArg() { + let arg = argv.shift(); + + if (arg.indexOf('--') === 0) { + // e.g. --opt + arg = arg.split('='); + if (arg.length > 1) { + // e.g. --opt=val + argv.unshift(arg.slice(1).join('=')); + } + arg = arg[0]; + } else if (arg[0] === '-') { + if (arg.length > 2) { + // e.g. -abc + argv = arg.substring(1).split('').map(function(ch) { + return '-' + ch; + }).concat(argv); + arg = argv.shift(); + } else { + // e.g. -a + } + } else { + // e.g. foo + } + + return arg; + } + + while (argv.length) { + arg = getArg(); + switch (arg) { + case '-o': + case '--output': + output = argv.shift(); + break; + case '-i': + case '--input': + input = argv.shift(); + break; + case '-s': + case '--string': + string = argv.shift(); + break; + case '-t': + case '--tokens': + tokens = true; + break; + case '-c': + case '--config': + config = argv.shift(); + break; + case '-h': + case '--help': + return await help(); + case '-v': + case '--version': + return await version(); + default: + if (arg.indexOf('--') === 0) { + opt = camelize(arg.replace(/^--(no-)?/, '')); + if (!marked.defaults.hasOwnProperty(opt)) { + continue; + } + if (arg.indexOf('--no-') === 0) { + options[opt] = typeof marked.defaults[opt] !== 'boolean' + ? null + : false; + } else { + options[opt] = typeof marked.defaults[opt] !== 'boolean' + ? argv.shift() + : true; + } + } else { + files.push(arg); + } + break; + } + } + + async function getData() { + if (!input) { + if (files.length <= 2) { + if (string) { + return string; + } + return await getStdin(); + } + input = files.pop(); + } + return await readFile(input, 'utf8'); + } + + function resolveFile(file) { + return resolve(file.replace(/^~/, homedir)); + } + + function fileExists(file) { + return access(resolveFile(file)).then(() => true, () => false); + } + + async function runConfig(file) { + const configFile = resolveFile(file); + let markedConfig; + try { + // try require for json + markedConfig = require(configFile); + } catch (err) { + if (err.code !== 'ERR_REQUIRE_ESM') { + throw err; + } + // must import esm + markedConfig = await import('file:///' + configFile); + } + + if (markedConfig.default) { + markedConfig = markedConfig.default; + } + + if (typeof markedConfig === 'function') { + markedConfig(marked); + } else { + marked.use(markedConfig); + } + } + + const data = await getData(); + + if (config) { + if (!await fileExists(config)) { + throw Error(`Cannot load config file '${config}'`); + } + + await runConfig(config); + } else { + const defaultConfig = [ + '~/.marked.json', + '~/.marked.js', + '~/.marked/index.js' + ]; + + for (const configFile of defaultConfig) { + if (await fileExists(configFile)) { + await runConfig(configFile); + break; + } + } + } + + const html = tokens + ? JSON.stringify(marked.lexer(data, options), null, 2) + : await marked.parse(data, options); + + if (output) { + return await writeFile(output, html); + } + + nodeProcess.stdout.write(html + '\n'); + } + + /** + * Helpers + */ + function getStdin() { + return new Promise((resolve, reject) => { + const stdin = nodeProcess.stdin; + let buff = ''; + + stdin.setEncoding('utf8'); + + stdin.on('data', function(data) { + buff += data; + }); + + stdin.on('error', function(err) { + reject(err); + }); + + stdin.on('end', function() { + resolve(buff); + }); + + stdin.resume(); + }); + } + + /** + * @param {string} text + */ + function camelize(text) { + return text.replace(/(\w)-(\w)/g, function(_, a, b) { + return a + b.toUpperCase(); + }); + } + + try { + await start(nodeProcess.argv.slice()); + nodeProcess.exit(0); + } catch (err) { + if (err.code === 'ENOENT') { + nodeProcess.stderr.write('marked: output to ' + err.path + ': No such directory'); + } + nodeProcess.stderr.write(err); + return nodeProcess.exit(1); + } +} diff --git a/bin/marked.js b/bin/marked.js index 5031246258..e2dd816f6b 100755 --- a/bin/marked.js +++ b/bin/marked.js @@ -5,213 +5,11 @@ * Copyright (c) 2011-2013, Christopher Jeffrey (MIT License) */ -import { promises } from 'fs'; -import { marked } from '../lib/marked.esm.js'; - -const { readFile, writeFile } = promises; - -/** - * Man Page - */ - -async function help() { - const { spawn } = await import('child_process'); - - const options = { - cwd: process.cwd(), - env: process.env, - setsid: false, - stdio: 'inherit' - }; - - const { dirname, resolve } = await import('path'); - const { fileURLToPath } = await import('url'); - const __dirname = dirname(fileURLToPath(import.meta.url)); - const helpText = await readFile(resolve(__dirname, '../man/marked.1.txt'), 'utf8'); - - // eslint-disable-next-line promise/param-names - await new Promise(res => { - spawn('man', [resolve(__dirname, '../man/marked.1')], options) - .on('error', () => { - console.log(helpText); - }) - .on('close', res); - }); -} - -async function version() { - const { createRequire } = await import('module'); - const require = createRequire(import.meta.url); - const pkg = require('../package.json'); - console.log(pkg.version); -} - -/** - * Main - */ - -async function main(argv) { - const files = []; - const options = {}; - let input; - let output; - let string; - let arg; - let tokens; - let opt; - - function getarg() { - let arg = argv.shift(); - - if (arg.indexOf('--') === 0) { - // e.g. --opt - arg = arg.split('='); - if (arg.length > 1) { - // e.g. --opt=val - argv.unshift(arg.slice(1).join('=')); - } - arg = arg[0]; - } else if (arg[0] === '-') { - if (arg.length > 2) { - // e.g. -abc - argv = arg.substring(1).split('').map(function(ch) { - return '-' + ch; - }).concat(argv); - arg = argv.shift(); - } else { - // e.g. -a - } - } else { - // e.g. foo - } - - return arg; - } - - while (argv.length) { - arg = getarg(); - switch (arg) { - case '-o': - case '--output': - output = argv.shift(); - break; - case '-i': - case '--input': - input = argv.shift(); - break; - case '-s': - case '--string': - string = argv.shift(); - break; - case '-t': - case '--tokens': - tokens = true; - break; - case '-h': - case '--help': - return await help(); - case '-v': - case '--version': - return await version(); - default: - if (arg.indexOf('--') === 0) { - opt = camelize(arg.replace(/^--(no-)?/, '')); - if (!marked.defaults.hasOwnProperty(opt)) { - continue; - } - if (arg.indexOf('--no-') === 0) { - options[opt] = typeof marked.defaults[opt] !== 'boolean' - ? null - : false; - } else { - options[opt] = typeof marked.defaults[opt] !== 'boolean' - ? argv.shift() - : true; - } - } else { - files.push(arg); - } - break; - } - } - - async function getData() { - if (!input) { - if (files.length <= 2) { - if (string) { - return string; - } - return await getStdin(); - } - input = files.pop(); - } - return await readFile(input, 'utf8'); - } - - const data = await getData(); - - const html = tokens - ? JSON.stringify(marked.lexer(data, options), null, 2) - : marked(data, options); - - if (output) { - return await writeFile(output, html); - } - - process.stdout.write(html + '\n'); -} - -/** - * Helpers - */ - -function getStdin() { - return new Promise((resolve, reject) => { - const stdin = process.stdin; - let buff = ''; - - stdin.setEncoding('utf8'); - - stdin.on('data', function(data) { - buff += data; - }); - - stdin.on('error', function(err) { - reject(err); - }); - - stdin.on('end', function() { - resolve(buff); - }); - - stdin.resume(); - }); -} - -/** - * @param {string} text - */ -function camelize(text) { - return text.replace(/(\w)-(\w)/g, function(_, a, b) { - return a + b.toUpperCase(); - }); -} - -function handleError(err) { - if (err.code === 'ENOENT') { - console.error('marked: output to ' + err.path + ': No such directory'); - return process.exit(1); - } - throw err; -} +import { main } from './main.js'; /** * Expose / Entry Point */ process.title = 'marked'; -main(process.argv.slice()).then(code => { - process.exit(code || 0); -}).catch(err => { - handleError(err); -}); +main(process); diff --git a/docs/INDEX.md b/docs/INDEX.md index 2d07d75d3f..64497a5698 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -75,6 +75,30 @@ $ cat readme.html $ marked --help ``` +*CLI Config* + +A config file can be used to configure the marked cli. + +If it is a `.json` file it should be a JSON object that will be passed to marked as options. + +If `.js` is used it should have a default export of a marked options object or a function that takes `marked` as a parameter. +It can use the `marked` parameter to install extensions using `marked.use`. + +By default the marked cli will look for a config file in your home directory in the following order. + +- `~/.marked.json` +- `~/.marked.js` +- `~/.marked/index.js` + +```bash +# Example with custom config + +echo '{ "breaks": true }' > config.json + +$ marked -s 'line1\nline2' -c config.json +
line1
line2