Skip to content

Commit

Permalink
add config option to bin/marked (#2937)
Browse files Browse the repository at this point in the history
* add config option to bin/marked

* add tests

* add docs

* remove focused tests
  • Loading branch information
UziTech authored Sep 3, 2023
1 parent afbaebc commit 65b6f14
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 205 deletions.
270 changes: 270 additions & 0 deletions bin/main.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

1 comment on commit 65b6f14

@vercel
Copy link

@vercel vercel bot commented on 65b6f14 Sep 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.