single-file-cli 2.0.68 → 2.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,7 +41,7 @@ function initSingleFile() {
41
41
  xhrRequest.onreadystatechange = () => {
42
42
  if (xhrRequest.readyState == XMLHttpRequest.DONE) {
43
43
  resolve({
44
- arrayBuffer: async () => xhrRequest.response || new ArrayBuffer(),
44
+ arrayBuffer: () => xhrRequest.response || new ArrayBuffer(),
45
45
  headers: { get: headerName => xhrRequest.getResponseHeader(headerName) },
46
46
  status: xhrRequest.status
47
47
  });
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "2.0.68";
1
+ export const version = "2.0.70";
package/options.js CHANGED
@@ -42,6 +42,7 @@ const OPTIONS_INFO = {
42
42
  "block-scripts": { description: "Block scripts", type: "boolean", defaultValue: true },
43
43
  "block-videos": { description: "Block videos", type: "boolean", defaultValue: true },
44
44
  "block-mixed-content": { description: "Block mixed contents", type: "boolean" },
45
+ "blocked-URL-pattern": { description: "Regular expression matching URLs to block (e.g. 'annoying-banners\\.com')", type: "string[]" },
45
46
  "browser-server": { description: "Server to connect to", type: "string" },
46
47
  "browser-headless": { description: "Run the browser in headless mode", type: "boolean", defaultValue: true },
47
48
  "browser-executable-path": { description: "Path to chrome/chromium executable", type: "string" },
@@ -50,6 +51,7 @@ const OPTIONS_INFO = {
50
51
  "browser-load-max-time": { description: "Maximum delay of time to wait for page loading in ms", type: "number", defaultValue: 60000 },
51
52
  "browser-wait-delay": { description: "Time to wait before capturing the page in ms", type: "number" },
52
53
  "browser-wait-until": { description: "When to consider the page is loaded (InteractiveTime, networkIdle, networkAlmostIdle, load, domContentLoaded)", type: "string", defaultValue: "networkIdle" },
54
+ "browser-wait-until-delay": { description: "Delay of time in ms to wait before considering the page is loaded when the value of --browser-wait-until is reached", type: "number", defaultValue: 1000 },
53
55
  "browser-wait-until-fallback": { description: "Retry with the next value of --browser-wait-until when a timeout error is thrown", type: "boolean", defaultValue: true },
54
56
  "browser-debug": { description: "Enable debug mode", type: "boolean" },
55
57
  "browser-script": { description: "Path of a script executed in the page (and all the frames) before it is loaded", type: "string[]" },
@@ -66,6 +68,7 @@ const OPTIONS_INFO = {
66
68
  "browser-device-height": { description: "Height of the device viewport in pixels (default value is 800 when using --browser-mobile-emulation)", type: "number" },
67
69
  "browser-device-scale-factor": { description: "Scale factor of the device viewport (default value is 2 when using --browser-mobile-emulation)", type: "number" },
68
70
  "console-messages-file": { description: "Path of the file where to save the console messages in JSON format", type: "string" },
71
+ "debug-messages-file": { description: "Path of the file where to save the debug messages", type: "string" },
69
72
  "compress-CSS": { description: "Compress CSS stylesheets", type: "boolean" },
70
73
  "compress-HTML": { description: "Compress HTML content", type: "boolean", defaultValue: true },
71
74
  "crawl-links": { description: "Crawl and save pages found via inner links", type: "boolean" },
@@ -121,7 +124,7 @@ const OPTIONS_INFO = {
121
124
  "remove-alternative-images": { description: "Remove images for alternative sizes of screen", type: "boolean", defaultValue: true },
122
125
  "save-original-URLs": { description: "Save the original URLS in the embedded contents", type: "boolean" },
123
126
  "save-raw-page": { description: "Save the original page without interpreting it into the browser", type: "boolean" },
124
- "urls-file": { description: "Path to a text file containing a list of URLs (separated by a newline) to save", type: "string" },
127
+ "urls-file": { description: "Path to a text file containing a list of URLs (separated by a newline) to save. You can also pass the options after each URL (e.g. 'https://www.example.com --filename-template={page-title}.html')", type: "string" },
125
128
  "user-agent": { description: "User-agent of the browser", type: "string" },
126
129
  "accept-language": { description: "Accept language of the browser", type: "string" },
127
130
  "platform": { description: "Platform of the browser (default value is \"Android\" when using --browser-mobile-emulation)", type: "string" },
@@ -145,14 +148,14 @@ const OPTIONS_INFO = {
145
148
  "resolve-links": { description: "Resolve link URLs to absolute URLs", type: "boolean", defaultValue: true },
146
149
  "settings-file": { description: "Path to a JSON file containing the settings exported from the web extension", type: "string" },
147
150
  "settings-file-profile": { description: "Name of the profile to use when using --settings-file", type: "string", defaultValue: "default" },
148
- "group-duplicate-stylesheets": { description: "Group duplicate inline stylesheets into a single stylesheet in order to reduce the size of the page", type: "boolean", defaultValue: true }
151
+ "group-duplicate-stylesheets": { description: "Group duplicate inline stylesheets into a single stylesheet in order to reduce the size of the page", type: "boolean", defaultValue: false }
149
152
  };
150
153
 
151
154
  const { args, exit } = Deno;
152
- const options = await getOptions();
153
- export default options;
155
+ const options = getOptions();
156
+ export { options, parseArgs };
154
157
 
155
- async function getOptions() {
158
+ function getOptions() {
156
159
  const { positionals, options } = parseArgs(Array.from(args));
157
160
  const unknownOptions = [];
158
161
  positionals.forEach(positional => {
@@ -185,47 +188,10 @@ async function getOptions() {
185
188
  console.log(version); // eslint-disable-line no-console
186
189
  exit(0);
187
190
  }
188
- Object.keys(OPTIONS_INFO).forEach(optionName => {
189
- const optionInfo = getOptionInfo(optionName);
190
- const optionKey = getOptionKey(optionName, optionInfo);
191
- if (options[optionKey] === undefined && optionInfo.defaultValue !== undefined) {
192
- options[optionKey] = OPTIONS_INFO[optionName].defaultValue;
193
- }
194
- });
195
- options.acceptHeaders = {
196
- font: options.acceptHeaderFont,
197
- image: options.acceptHeaderImage,
198
- stylesheet: options.acceptHeaderStylesheet,
199
- script: options.acceptHeaderScript,
200
- document: options.acceptHeaderDocument
201
- };
202
- if (options.browserArgs) {
203
- const browserArguments = options.browserArguments || [];
204
- browserArguments.push(...JSON.parse(options.browserArgs));
205
- options.browserArgs = browserArguments;
206
- delete options.browserArguments;
207
- }
208
- if (options.browserArguments) {
209
- options.browserArgs = options.browserArguments;
210
- delete options.browserArguments;
211
- }
212
- if (options.errorFile) {
213
- options.errorsFile = options.errorFile;
214
- delete options.errorFile;
215
- }
216
- if (options.errorTracesDisabled) {
217
- options.errorsTracesDisabled = options.errorTracesDisabled;
218
- delete options.errorTracesDisabled;
219
- }
220
- delete options.acceptHeaderFont;
221
- delete options.acceptHeaderImage;
222
- delete options.acceptHeaderStylesheet;
223
- delete options.acceptHeaderScript;
224
- delete options.acceptHeaderDocument;
225
191
  return { ...options, url: positionals[0], output: positionals[1] };
226
192
  }
227
193
 
228
- function parseArgs(args) {
194
+ function parseArgs(args, setDefaultValues = true) {
229
195
  const positionals = [];
230
196
  const options = {};
231
197
  const result = { positionals, options: {} };
@@ -280,6 +246,83 @@ function parseArgs(args) {
280
246
  }
281
247
  result.options[optionKey] = optionValue;
282
248
  });
249
+ if (setDefaultValues) {
250
+ Object.keys(OPTIONS_INFO).forEach(optionName => {
251
+ const optionInfo = getOptionInfo(optionName);
252
+ const optionKey = getOptionKey(optionName, optionInfo);
253
+ if (result.options[optionKey] === undefined && optionInfo.defaultValue !== undefined) {
254
+ result.options[optionKey] = OPTIONS_INFO[optionName].defaultValue;
255
+ }
256
+ });
257
+ }
258
+ if (result.options.acceptHeaderFont ||
259
+ result.options.acceptHeaderImage ||
260
+ result.options.acceptHeaderStylesheet ||
261
+ result.options.acceptHeaderScript ||
262
+ result.options.acceptHeaderDocument) {
263
+ result.options.acceptHeaders = {
264
+ font: result.options.acceptHeaderFont,
265
+ image: result.options.acceptHeaderImage,
266
+ stylesheet: result.options.acceptHeaderStylesheet,
267
+ script: result.options.acceptHeaderScript,
268
+ document: result.options.acceptHeaderDocument
269
+ };
270
+ delete result.options.acceptHeaderFont;
271
+ delete result.options.acceptHeaderImage;
272
+ delete result.options.acceptHeaderStylesheet;
273
+ delete result.options.acceptHeaderScript;
274
+ delete result.options.acceptHeaderDocument;
275
+ }
276
+ if (result.options.browserArgs) {
277
+ const browserArguments = result.options.browserArguments || [];
278
+ browserArguments.push(...JSON.parse(result.options.browserArgs));
279
+ result.options.browserArgs = browserArguments;
280
+ delete result.options.browserArguments;
281
+ }
282
+ if (result.options.browserArguments) {
283
+ result.options.browserArgs = result.options.browserArguments;
284
+ delete result.options.browserArguments;
285
+ }
286
+ if (result.options.errorFile) {
287
+ result.options.errorsFile = result.options.errorFile;
288
+ delete result.options.errorFile;
289
+ }
290
+ if (result.options.errorTracesDisabled) {
291
+ result.options.errorsTracesDisabled = result.options.errorTracesDisabled;
292
+ delete result.options.errorTracesDisabled;
293
+ }
294
+ if (result.options.crawlReplaceUrls) {
295
+ result.options.crawlReplaceURLs = result.options.crawlReplaceUrls;
296
+ delete result.options.crawlReplaceUrls;
297
+ }
298
+ if (result.options.saveOriginalUrls) {
299
+ result.options.saveOriginalURLs = result.options.saveOriginalUrls;
300
+ delete result.options.saveOriginalUrls;
301
+ }
302
+ if (result.options.browserRemoteDebuggingUrl) {
303
+ result.options.browserRemoteDebuggingURL = result.options.browserRemoteDebuggingUrl;
304
+ delete result.options.browserRemoteDebuggingUrl;
305
+ }
306
+ if (result.options.crawlRemoveUrlFragment) {
307
+ result.options.crawlRemoveURLFragment = result.options.crawlRemoveUrlFragment;
308
+ delete result.options.crawlRemoveUrlFragment;
309
+ }
310
+ if (result.options.compressCss) {
311
+ result.options.compressCSS = result.options.compressCss;
312
+ delete result.options.compressCss;
313
+ }
314
+ if (result.options.compressHtml) {
315
+ result.options.compressHTML = result.options.compressHtml;
316
+ delete result.options.compressHtml;
317
+ }
318
+ if (result.options.insertMetaCsp) {
319
+ result.options.insertMetaCSP = result.options.insertMetaCsp;
320
+ delete result.options.insertMetaCsp;
321
+ }
322
+ if (result.options.blockedUrlPatterns) {
323
+ result.options.blockedURLPatterns = result.options.blockedUrlPatterns;
324
+ delete result.options.blockedUrlPatterns;
325
+ }
283
326
  return result;
284
327
  }
285
328
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-cli",
3
- "version": "2.0.68",
3
+ "version": "2.0.70",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "engines": {
@@ -39,8 +39,9 @@ const DEFAULT_OPTIONS = {
39
39
  filenameTemplate: "{page-title} ({date-locale} {time-locale}).html",
40
40
  filenameMaxLength: 192,
41
41
  filenameMaxLengthUnit: "bytes",
42
- filenameReplacedCharacters: ["~", "+", "\\\\", "?", "%", "*", ":", "|", "\"", "<", ">", "\x00-\x1f", "\x7F"],
42
+ filenameReplacedCharacters: ["~", "+", "?", "%", "*", ":", "|", "\"", "<", ">", "\\\\", "\x00-\x1f", "\x7F"],
43
43
  filenameReplacementCharacter: "_",
44
+ filenameReplacementCharacters: ["~", "+", "?", "%", "*", ":", "|", """, "<", ">", "\"],
44
45
  maxResourceSize: 10,
45
46
  backgroundSave: true,
46
47
  removeAlternativeFonts: true,
@@ -101,7 +102,17 @@ async function initialize(options) {
101
102
  async function capture(urls, options) {
102
103
  let newTasks;
103
104
  const taskUrls = tasks.map(task => task.url);
104
- newTasks = await Promise.all(urls.map(url => createTask(url, options)));
105
+ newTasks = await Promise.all(urls.map(value => {
106
+ let url, taskOptions;
107
+ if (Array.isArray(value)) {
108
+ url = value[0];
109
+ taskOptions = Object.assign({}, options, value[1]);
110
+ } else {
111
+ url = value;
112
+ taskOptions = options;
113
+ }
114
+ return createTask(url, taskOptions);
115
+ }));
105
116
  newTasks = newTasks.filter(task => task && !taskUrls.includes(task.url));
106
117
  if (newTasks.length) {
107
118
  tasks = tasks.concat(newTasks);
@@ -127,7 +138,7 @@ async function finish(options) {
127
138
  }
128
139
  });
129
140
  await writeTextFile(task.filename, pageContent);
130
- } catch (error) {
141
+ } catch {
131
142
  // ignored
132
143
  }
133
144
  }
@@ -137,7 +148,7 @@ async function finish(options) {
137
148
  }
138
149
  }
139
150
 
140
- async function runTasks() {
151
+ function runTasks() {
141
152
  const availableTasks = tasks.filter(task => !task.status).length;
142
153
  const processingTasks = tasks.filter(task => task.status == STATE_PROCESSING).length;
143
154
  const promisesTasks = [];
@@ -151,7 +162,7 @@ async function runNextTask() {
151
162
  const task = tasks.find(task => !task.status);
152
163
  if (task) {
153
164
  const options = task.options;
154
- let taskOptions = JSON.parse(JSON.stringify(options));
165
+ const taskOptions = JSON.parse(JSON.stringify(options));
155
166
  taskOptions.url = task.url;
156
167
  task.status = STATE_PROCESSING;
157
168
  await saveTasks();
@@ -166,6 +177,7 @@ async function runNextTask() {
166
177
  newTasks = newTasks.filter(task => task &&
167
178
  testMaxDepth(task) &&
168
179
  !tasks.find(otherTask => otherTask.url == task.url) &&
180
+ !newTasks.find(otherTask => otherTask != task && otherTask.url == task.url) &&
169
181
  (!options.crawlInnerLinksOnly || task.isInnerLink) &&
170
182
  (!options.crawlNoParent || (task.isChild || !task.isInnerLink)));
171
183
  tasks.splice(tasks.length, 0, ...newTasks);
@@ -251,6 +263,10 @@ async function capturePage(options) {
251
263
  if (options.consoleMessagesFile && pageData.consoleMessages) {
252
264
  await writeTextFile(options.consoleMessagesFile, JSON.stringify(pageData.consoleMessages, null, 2));
253
265
  }
266
+ if (options.debugMessagesFile && pageData.debugMessages) {
267
+ await writeTextFile(options.debugMessagesFile, pageData.debugMessages.map(([timestamp, message]) =>
268
+ `[${new Date(timestamp).toISOString()}] ${message.join(" ")}`).join("\n"));
269
+ }
254
270
  if (options.outputJson) {
255
271
  if (content instanceof Uint8Array) {
256
272
  const fileReader = new FileReader();
@@ -282,7 +298,7 @@ async function capturePage(options) {
282
298
  if (options.outputJson) {
283
299
  filename += filename.endsWith(".json") ? "" : ".json";
284
300
  }
285
- const directoryName = await path.dirname(filename);
301
+ const directoryName = path.dirname(filename);
286
302
  if (directoryName !== ".") {
287
303
  await mkdir(directoryName, { recursive: true });
288
304
  }
@@ -305,6 +321,13 @@ async function capturePage(options) {
305
321
  } else {
306
322
  console.error(error.message || error, message); // eslint-disable-line no-console
307
323
  }
324
+ if (options.consoleMessagesFile && error.consoleMessages) {
325
+ await writeTextFile(options.consoleMessagesFile, JSON.stringify(error.consoleMessages, null, 2));
326
+ }
327
+ if (options.debugMessagesFile && error.debugMessages) {
328
+ await writeTextFile(options.debugMessagesFile, error.debugMessages.map(([timestamp, message]) =>
329
+ `[${new Date(timestamp).toISOString()}] ${message.join(" ")}`).join("\n"));
330
+ }
308
331
  }
309
332
  }
310
333
 
@@ -338,7 +361,7 @@ async function getFilename(filename, options, index = 1) {
338
361
  if (options.filenameConflictAction != "skip") {
339
362
  return getFilename(filename, options, index + 1);
340
363
  }
341
- } catch (_) {
364
+ } catch {
342
365
  return newFilename;
343
366
  }
344
367
  }
@@ -24,7 +24,7 @@
24
24
  import { initialize } from "./single-file-cli-api.js";
25
25
  import { closeBrowser } from "./lib/browser.js";
26
26
  import { Deno } from "./lib/deno-polyfill.js";
27
- import options from "./options.js";
27
+ import { options, parseArgs } from "./options.js";
28
28
 
29
29
  const { readTextFile, readFile, exit, addSignalListener } = Deno;
30
30
 
@@ -54,7 +54,7 @@ async function run() {
54
54
  delete options.settingsFile;
55
55
  }
56
56
  if (options.urlsFile) {
57
- urls = (await readTextFile(options.urlsFile)).split("\n");
57
+ urls = await getUrlsFile(options.urlsFile);
58
58
  } else {
59
59
  urls = [options.url];
60
60
  }
@@ -80,7 +80,7 @@ async function run() {
80
80
  const cookiesContent = await readTextFile(options.browserCookiesFile);
81
81
  try {
82
82
  options.browserCookies = JSON.parse(cookiesContent);
83
- } catch (error) {
83
+ } catch {
84
84
  options.browserCookies = parseCookies(cookiesContent);
85
85
  }
86
86
  }
@@ -137,3 +137,56 @@ async function closeBrowserAndExit(code) {
137
137
  await closeBrowser();
138
138
  exit(code);
139
139
  }
140
+
141
+ async function getUrlsFile(urlsFile) {
142
+ let urls = (await readTextFile(urlsFile)).split("\n");
143
+ urls = urls.map(value => {
144
+ value = value.trim();
145
+ let optionPosition = value.indexOf(" --");
146
+ if (optionPosition < 0) {
147
+ optionPosition = value.indexOf("\t--");
148
+ }
149
+ if (optionPosition > 0) {
150
+ const url = value.substring(0, optionPosition).trim();
151
+ const argsString = value.substring(optionPosition + 1).trim();
152
+ const args = [];
153
+ let previousCharacter, previousPreviousCharacter, lastQuoteCharacter;
154
+ let lastCharIndex = 0;
155
+ for (let currentCharIndex = 0; currentCharIndex < argsString.length; currentCharIndex++) {
156
+ const character = argsString[currentCharIndex];
157
+ if (character == lastQuoteCharacter && (previousCharacter != "\\" || previousPreviousCharacter == "\\")) {
158
+ args.push(argsString.substring(lastCharIndex, currentCharIndex));
159
+ lastQuoteCharacter = null;
160
+ lastCharIndex = currentCharIndex + 1;
161
+ } else if (!lastQuoteCharacter) {
162
+ if (character == "'" || character == "\"") {
163
+ lastQuoteCharacter = argsString[currentCharIndex];
164
+ lastCharIndex = currentCharIndex + 1;
165
+ } else {
166
+ const isSpaceCharacter = character == " " || character == "\t";
167
+ if (isSpaceCharacter || character == "=") {
168
+ if (isSpaceCharacter && (currentCharIndex == lastCharIndex + 1)) {
169
+ lastCharIndex++;
170
+ } else if (lastCharIndex < currentCharIndex) {
171
+ args.push(argsString.substring(lastCharIndex, currentCharIndex));
172
+ lastCharIndex = currentCharIndex + 1;
173
+ } else {
174
+ lastCharIndex = currentCharIndex + 1;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ previousPreviousCharacter = previousCharacter;
180
+ previousCharacter = character;
181
+ }
182
+ if (lastCharIndex < argsString.length) {
183
+ args.push(argsString.substring(lastCharIndex).trim());
184
+ }
185
+ const { options } = parseArgs(args, false);
186
+ return [url, options];
187
+ } else {
188
+ return value;
189
+ }
190
+ });
191
+ return urls;
192
+ }