UNPKG

6.23 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3/**
4 * Marked CLI
5 * Copyright (c) 2011-2013, Christopher Jeffrey (MIT License)
6 */
7
8import { promises } from 'node:fs';
9import { dirname, resolve } from 'node:path';
10import { homedir } from 'node:os';
11import { createRequire } from 'node:module';
12import { marked } from '../lib/marked.esm.js';
13
14const { access, readFile, writeFile } = promises;
15const require = createRequire(import.meta.url);
16
17/**
18 * @param {Process} nodeProcess inject process so it can be mocked in tests.
19 */
20export async function main(nodeProcess) {
21 /**
22 * Man Page
23 */
24 async function help() {
25 const { spawn } = await import('child_process');
26 const { fileURLToPath } = await import('url');
27
28 const options = {
29 cwd: nodeProcess.cwd(),
30 env: nodeProcess.env,
31 stdio: 'inherit'
32 };
33
34 const __dirname = dirname(fileURLToPath(import.meta.url));
35 const helpText = await readFile(resolve(__dirname, '../man/marked.1.txt'), 'utf8');
36
37 // eslint-disable-next-line promise/param-names
38 await new Promise(res => {
39 spawn('man', [resolve(__dirname, '../man/marked.1')], options)
40 .on('error', () => {
41 console.log(helpText);
42 })
43 .on('close', res);
44 });
45 }
46
47 async function version() {
48 const pkg = require('../package.json');
49 console.log(pkg.version);
50 }
51
52 /**
53 * Main
54 */
55 async function start(argv) {
56 const files = [];
57 const options = {};
58 let input;
59 let output;
60 let string;
61 let arg;
62 let tokens;
63 let config;
64 let opt;
65
66 function getArg() {
67 let arg = argv.shift();
68
69 if (arg.indexOf('--') === 0) {
70 // e.g. --opt
71 arg = arg.split('=');
72 if (arg.length > 1) {
73 // e.g. --opt=val
74 argv.unshift(arg.slice(1).join('='));
75 }
76 arg = arg[0];
77 } else if (arg[0] === '-') {
78 if (arg.length > 2) {
79 // e.g. -abc
80 argv = arg.substring(1).split('').map(function(ch) {
81 return '-' + ch;
82 }).concat(argv);
83 arg = argv.shift();
84 } else {
85 // e.g. -a
86 }
87 } else {
88 // e.g. foo
89 }
90
91 return arg;
92 }
93
94 while (argv.length) {
95 arg = getArg();
96 switch (arg) {
97 case '-o':
98 case '--output':
99 output = argv.shift();
100 break;
101 case '-i':
102 case '--input':
103 input = argv.shift();
104 break;
105 case '-s':
106 case '--string':
107 string = argv.shift();
108 break;
109 case '-t':
110 case '--tokens':
111 tokens = true;
112 break;
113 case '-c':
114 case '--config':
115 config = argv.shift();
116 break;
117 case '-h':
118 case '--help':
119 return await help();
120 case '-v':
121 case '--version':
122 return await version();
123 default:
124 if (arg.indexOf('--') === 0) {
125 opt = camelize(arg.replace(/^--(no-)?/, ''));
126 if (!marked.defaults.hasOwnProperty(opt)) {
127 continue;
128 }
129 if (arg.indexOf('--no-') === 0) {
130 options[opt] = typeof marked.defaults[opt] !== 'boolean'
131 ? null
132 : false;
133 } else {
134 options[opt] = typeof marked.defaults[opt] !== 'boolean'
135 ? argv.shift()
136 : true;
137 }
138 } else {
139 files.push(arg);
140 }
141 break;
142 }
143 }
144
145 async function getData() {
146 if (!input) {
147 if (files.length <= 2) {
148 if (string) {
149 return string;
150 }
151 return await getStdin();
152 }
153 input = files.pop();
154 }
155 return await readFile(input, 'utf8');
156 }
157
158 function resolveFile(file) {
159 return resolve(file.replace(/^~/, homedir));
160 }
161
162 function fileExists(file) {
163 return access(resolveFile(file)).then(() => true, () => false);
164 }
165
166 async function runConfig(file) {
167 const configFile = resolveFile(file);
168 let markedConfig;
169 try {
170 // try require for json
171 markedConfig = require(configFile);
172 } catch (err) {
173 if (err.code !== 'ERR_REQUIRE_ESM') {
174 throw err;
175 }
176 // must import esm
177 markedConfig = await import('file:///' + configFile);
178 }
179
180 if (markedConfig.default) {
181 markedConfig = markedConfig.default;
182 }
183
184 if (typeof markedConfig === 'function') {
185 markedConfig(marked);
186 } else {
187 marked.use(markedConfig);
188 }
189 }
190
191 const data = await getData();
192
193 if (config) {
194 if (!await fileExists(config)) {
195 throw Error(`Cannot load config file '${config}'`);
196 }
197
198 await runConfig(config);
199 } else {
200 const defaultConfig = [
201 '~/.marked.json',
202 '~/.marked.js',
203 '~/.marked/index.js'
204 ];
205
206 for (const configFile of defaultConfig) {
207 if (await fileExists(configFile)) {
208 await runConfig(configFile);
209 break;
210 }
211 }
212 }
213
214 const html = tokens
215 ? JSON.stringify(marked.lexer(data, options), null, 2)
216 : await marked.parse(data, options);
217
218 if (output) {
219 return await writeFile(output, html);
220 }
221
222 nodeProcess.stdout.write(html + '\n');
223 }
224
225 /**
226 * Helpers
227 */
228 function getStdin() {
229 return new Promise((resolve, reject) => {
230 const stdin = nodeProcess.stdin;
231 let buff = '';
232
233 stdin.setEncoding('utf8');
234
235 stdin.on('data', function(data) {
236 buff += data;
237 });
238
239 stdin.on('error', function(err) {
240 reject(err);
241 });
242
243 stdin.on('end', function() {
244 resolve(buff);
245 });
246
247 stdin.resume();
248 });
249 }
250
251 /**
252 * @param {string} text
253 */
254 function camelize(text) {
255 return text.replace(/(\w)-(\w)/g, function(_, a, b) {
256 return a + b.toUpperCase();
257 });
258 }
259
260 try {
261 await start(nodeProcess.argv.slice());
262 nodeProcess.exit(0);
263 } catch (err) {
264 if (err.code === 'ENOENT') {
265 nodeProcess.stderr.write('marked: output to ' + err.path + ': No such directory');
266 }
267 nodeProcess.stderr.write(err);
268 return nodeProcess.exit(1);
269 }
270}