UNPKG

8.17 kBJavaScriptView Raw
1/*
2 * Copyright (C) 2017 Alasdair Mercer, !ninja
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in all
12 * copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 * SOFTWARE.
21 */
22
23'use strict';
24
25const chalk = require('chalk');
26const { Command } = require('commander');
27const { EOL } = require('os');
28const fs = require('fs');
29const getStdin = require('get-stdin').buffer;
30const glob = require('glob');
31const path = require('path');
32const util = require('util');
33
34const Converter = require('./Converter');
35
36const findFiles = util.promisify(glob);
37const writeFile = util.promisify(fs.writeFile);
38
39const _applyOptions = Symbol('applyOptions');
40const _baseDir = Symbol('baseDir');
41const _command = Symbol('command');
42const _convertFiles = Symbol('convertFiles');
43const _convertInput = Symbol('convertInput');
44const _errorStream = Symbol('errorStream');
45const _outputStream = Symbol('outputStream');
46const _parseOptions = Symbol('parseOptions');
47const _provider = Symbol('provider');
48
49/**
50 * The command line interface for a SVG converter {@link Provider}.
51 *
52 * While technically part of the API, this is not expected to be used outside of this package as it's only intended use
53 * is by <code>bin/convert-svg-to-*</code>.
54 *
55 * @public
56 */
57class CLI {
58
59 /**
60 * Creates an instance of {@link CLI} with the specified <code>provider</code>.
61 *
62 * <code>options</code> is primarily intended for testing purposes and it's not expected to be supplied in any
63 * real-world scenario.
64 *
65 * @param {Provider} provider - the {@link Provider} to be used
66 * @param {CLI~Options} [options] - the options to be used
67 * @public
68 */
69 constructor(provider, options = {}) {
70 this[_provider] = provider;
71 this[_baseDir] = options.baseDir || process.cwd();
72 this[_errorStream] = options.errorStream || process.stderr;
73 this[_outputStream] = options.outputStream || process.stdout;
74 this[_command] = new Command()
75 .version(provider.getVersion())
76 .usage('[options] [files...]');
77
78 const format = provider.getFormat();
79
80 this[_applyOptions]([
81 {
82 flags: '--no-color',
83 description: 'disables color output'
84 },
85 {
86 flags: '--background <color>',
87 description: 'specify background color for transparent regions in SVG'
88 },
89 {
90 flags: '--base-url <url>',
91 description: 'specify base URL to use for all relative URLs in SVG'
92 },
93 {
94 flags: '--filename <filename>',
95 description: `specify filename for the ${format} output when processing STDIN`
96 },
97 {
98 flags: '--height <value>',
99 description: `specify height for ${format}`
100 },
101 {
102 flags: '--scale <value>',
103 description: 'specify scale to apply to dimensions [1]',
104 transformer: parseInt
105 },
106 {
107 flags: '--width <value>',
108 description: `specify width for ${format}`
109 }
110 ]);
111 this[_applyOptions](provider.getCLIOptions());
112 }
113
114 /**
115 * Writes the specified <code>message</code> to the error stream for this {@link CLI}.
116 *
117 * @param {string} message - the message to be written to the error stream
118 * @return {void}
119 * @public
120 */
121 error(message) {
122 this[_errorStream].write(`${message}${EOL}`);
123 }
124
125 /**
126 * Writes the specified <code>message</code> to the output stream for this {@link CLI}.
127 *
128 * @param {string} message - the message to be written to the output stream
129 * @return {void}
130 * @public
131 */
132 output(message) {
133 this[_outputStream].write(`${message}${EOL}`);
134 }
135
136 /**
137 * Parses the command-line (process) arguments provided and performs the necessary actions based on the parsed input.
138 *
139 * An error will occur if any problem arises.
140 *
141 * @param {string[]} [args] - the arguments to be parsed
142 * @return {Promise.<void, Error>} A <code>Promise</code> that is resolved once all actions have been completed.
143 * @public
144 */
145 async parse(args = []) {
146 const command = this[_command].parse(args);
147 const converter = new Converter(this[_provider]);
148 const options = this[_parseOptions]();
149
150 try {
151 if (command.args.length) {
152 const filePaths = [];
153
154 for (const arg of command.args) {
155 const files = await findFiles(arg, {
156 absolute: true,
157 cwd: this[_baseDir],
158 nodir: true
159 });
160
161 filePaths.push(...files);
162 }
163
164 await this[_convertFiles](converter, filePaths, options);
165 } else {
166 const input = await getStdin();
167
168 await this[_convertInput](converter, input, options,
169 command.filename ? path.resolve(this[_baseDir], command.filename) : null);
170 }
171 } finally {
172 await converter.destroy();
173 }
174 }
175
176 [_applyOptions](options) {
177 if (!options) {
178 return;
179 }
180
181 for (const option of options) {
182 this[_command].option(option.flags, option.description, option.transformer);
183 }
184 }
185
186 async [_convertFiles](converter, filePaths, options) {
187 const format = this[_provider].getFormat();
188
189 for (const inputFilePath of filePaths) {
190 const outputFilePath = await converter.convertFile(inputFilePath, options);
191
192 this.output(`Converted SVG file to ${format} file: ` +
193 `${chalk.blue(inputFilePath)} -> ${chalk.blue(outputFilePath)}`);
194 }
195
196 this.output(chalk.green('Done!'));
197 }
198
199 async [_convertInput](converter, input, options, filePath) {
200 if (!options.baseUrl) {
201 options.baseFile = this[_baseDir];
202 }
203
204 const output = await converter.convert(input, options);
205
206 if (filePath) {
207 await writeFile(filePath, output);
208
209 this.output(`Converted SVG input to ${this[_provider].getFormat()} file: ${chalk.blue(filePath)}`);
210 this.output(chalk.green('Done!'));
211 } else {
212 this[_outputStream].write(output);
213 }
214 }
215
216 [_parseOptions]() {
217 const command = this[_command];
218 const options = {
219 background: command.background,
220 baseUrl: command.baseUrl,
221 height: command.height,
222 scale: command.scale,
223 width: command.width
224 };
225
226 this[_provider].parseCLIOptions(options, command);
227
228 return options;
229 }
230
231 /**
232 * Returns the {@link Provider} for this {@link CLI}.
233 *
234 * @return {Provider} The provider.
235 * @public
236 */
237 get provider() {
238 return this[_provider];
239 }
240
241}
242
243module.exports = CLI;
244
245/**
246 * Describes a CLI option.
247 *
248 * @typedef {Object} CLI~Option
249 * @property {string} description - The description to be used when displaying help information for this option.
250 * @property {string} flags - The flags to be accepted by this option.
251 * @property {Function} [transformer] - The function to be used to transform the argument of this option, where
252 * applicable. When omitted, the argument string value will be used as-is.
253 */
254
255/**
256 * The options that can be passed to the {@link CLI} constructor.
257 *
258 * @typedef {Object} CLI~Options
259 * @property {string} [baseDir=process.cwd()] - The base directory to be used.
260 * @property {Writable} [errorStream=process.stderr] - The stream for error messages to be written to.
261 * @property {Writable} [outputStream=process.stdout] - The stream for output messages to be written to.
262 */