single-file-cli 2.0.67 → 2.0.69

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.67";
1
+ export const version = "2.0.69";
package/options.js CHANGED
@@ -50,6 +50,7 @@ const OPTIONS_INFO = {
50
50
  "browser-load-max-time": { description: "Maximum delay of time to wait for page loading in ms", type: "number", defaultValue: 60000 },
51
51
  "browser-wait-delay": { description: "Time to wait before capturing the page in ms", type: "number" },
52
52
  "browser-wait-until": { description: "When to consider the page is loaded (InteractiveTime, networkIdle, networkAlmostIdle, load, domContentLoaded)", type: "string", defaultValue: "networkIdle" },
53
+ "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
54
  "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
55
  "browser-debug": { description: "Enable debug mode", type: "boolean" },
55
56
  "browser-script": { description: "Path of a script executed in the page (and all the frames) before it is loaded", type: "string[]" },
@@ -121,7 +122,7 @@ const OPTIONS_INFO = {
121
122
  "remove-alternative-images": { description: "Remove images for alternative sizes of screen", type: "boolean", defaultValue: true },
122
123
  "save-original-URLs": { description: "Save the original URLS in the embedded contents", type: "boolean" },
123
124
  "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" },
125
+ "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
126
  "user-agent": { description: "User-agent of the browser", type: "string" },
126
127
  "accept-language": { description: "Accept language of the browser", type: "string" },
127
128
  "platform": { description: "Platform of the browser (default value is \"Android\" when using --browser-mobile-emulation)", type: "string" },
@@ -149,10 +150,10 @@ const OPTIONS_INFO = {
149
150
  };
150
151
 
151
152
  const { args, exit } = Deno;
152
- const options = await getOptions();
153
- export default options;
153
+ const options = getOptions();
154
+ export { options, parseArgs };
154
155
 
155
- async function getOptions() {
156
+ function getOptions() {
156
157
  const { positionals, options } = parseArgs(Array.from(args));
157
158
  const unknownOptions = [];
158
159
  positionals.forEach(positional => {
@@ -185,43 +186,6 @@ async function getOptions() {
185
186
  console.log(version); // eslint-disable-line no-console
186
187
  exit(0);
187
188
  }
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
189
  return { ...options, url: positionals[0], output: positionals[1] };
226
190
  }
227
191
 
@@ -280,6 +244,71 @@ function parseArgs(args) {
280
244
  }
281
245
  result.options[optionKey] = optionValue;
282
246
  });
247
+ Object.keys(OPTIONS_INFO).forEach(optionName => {
248
+ const optionInfo = getOptionInfo(optionName);
249
+ const optionKey = getOptionKey(optionName, optionInfo);
250
+ if (result.options[optionKey] === undefined && optionInfo.defaultValue !== undefined) {
251
+ result.options[optionKey] = OPTIONS_INFO[optionName].defaultValue;
252
+ }
253
+ });
254
+ result.options.acceptHeaders = {
255
+ font: result.options.acceptHeaderFont,
256
+ image: result.options.acceptHeaderImage,
257
+ stylesheet: result.options.acceptHeaderStylesheet,
258
+ script: result.options.acceptHeaderScript,
259
+ document: result.options.acceptHeaderDocument
260
+ };
261
+ if (result.options.browserArgs) {
262
+ const browserArguments = result.options.browserArguments || [];
263
+ browserArguments.push(...JSON.parse(result.options.browserArgs));
264
+ result.options.browserArgs = browserArguments;
265
+ delete result.options.browserArguments;
266
+ }
267
+ if (result.options.browserArguments) {
268
+ result.options.browserArgs = result.options.browserArguments;
269
+ delete result.options.browserArguments;
270
+ }
271
+ if (result.options.errorFile) {
272
+ result.options.errorsFile = result.options.errorFile;
273
+ delete result.options.errorFile;
274
+ }
275
+ if (result.options.errorTracesDisabled) {
276
+ result.options.errorsTracesDisabled = result.options.errorTracesDisabled;
277
+ delete result.options.errorTracesDisabled;
278
+ }
279
+ if (result.options.crawlReplaceUrls) {
280
+ result.options.crawlReplaceURLs = result.options.crawlReplaceUrls;
281
+ delete result.options.crawlReplaceUrls;
282
+ }
283
+ if (result.options.saveOriginalUrls) {
284
+ result.options.saveOriginalURLs = result.options.saveOriginalUrls;
285
+ delete result.options.saveOriginalUrls;
286
+ }
287
+ if (result.options.browserRemoteDebuggingUrl) {
288
+ result.options.browserRemoteDebuggingURL = result.options.browserRemoteDebuggingUrl;
289
+ delete result.options.browserRemoteDebuggingUrl;
290
+ }
291
+ if (result.options.crawlRemoveUrlFragment) {
292
+ result.options.crawlRemoveURLFragment = result.options.crawlRemoveUrlFragment;
293
+ delete result.options.crawlRemoveUrlFragment;
294
+ }
295
+ if (result.options.compressCss) {
296
+ result.options.compressCSS = result.options.compressCss;
297
+ delete result.options.compressCss;
298
+ }
299
+ if (result.options.compressHtml) {
300
+ result.options.compressHTML = result.options.compressHtml;
301
+ delete result.options.compressHtml;
302
+ }
303
+ if (result.options.insertMetaCsp) {
304
+ result.options.insertMetaCSP = result.options.insertMetaCsp;
305
+ delete result.options.insertMetaCsp;
306
+ }
307
+ delete result.options.acceptHeaderFont;
308
+ delete result.options.acceptHeaderImage;
309
+ delete result.options.acceptHeaderStylesheet;
310
+ delete result.options.acceptHeaderScript;
311
+ delete result.options.acceptHeaderDocument;
283
312
  return result;
284
313
  }
285
314
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-cli",
3
- "version": "2.0.67",
3
+ "version": "2.0.69",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "engines": {
@@ -101,7 +101,17 @@ async function initialize(options) {
101
101
  async function capture(urls, options) {
102
102
  let newTasks;
103
103
  const taskUrls = tasks.map(task => task.url);
104
- newTasks = await Promise.all(urls.map(url => createTask(url, options)));
104
+ newTasks = await Promise.all(urls.map(value => {
105
+ let url, taskOptions;
106
+ if (Array.isArray(value)) {
107
+ url = value[0];
108
+ taskOptions = Object.assign({}, options, value[1]);
109
+ } else {
110
+ url = value;
111
+ taskOptions = options;
112
+ }
113
+ return createTask(url, taskOptions);
114
+ }));
105
115
  newTasks = newTasks.filter(task => task && !taskUrls.includes(task.url));
106
116
  if (newTasks.length) {
107
117
  tasks = tasks.concat(newTasks);
@@ -127,7 +137,7 @@ async function finish(options) {
127
137
  }
128
138
  });
129
139
  await writeTextFile(task.filename, pageContent);
130
- } catch (error) {
140
+ } catch {
131
141
  // ignored
132
142
  }
133
143
  }
@@ -137,7 +147,7 @@ async function finish(options) {
137
147
  }
138
148
  }
139
149
 
140
- async function runTasks() {
150
+ function runTasks() {
141
151
  const availableTasks = tasks.filter(task => !task.status).length;
142
152
  const processingTasks = tasks.filter(task => task.status == STATE_PROCESSING).length;
143
153
  const promisesTasks = [];
@@ -151,7 +161,7 @@ async function runNextTask() {
151
161
  const task = tasks.find(task => !task.status);
152
162
  if (task) {
153
163
  const options = task.options;
154
- let taskOptions = JSON.parse(JSON.stringify(options));
164
+ const taskOptions = JSON.parse(JSON.stringify(options));
155
165
  taskOptions.url = task.url;
156
166
  task.status = STATE_PROCESSING;
157
167
  await saveTasks();
@@ -162,10 +172,11 @@ async function runNextTask() {
162
172
  task.filename = pageData.filename;
163
173
  if (options.crawlLinks && testMaxDepth(task)) {
164
174
  const urls = pageData.links;
165
- let newTasks = await Promise.all(urls.map(url => createTask(url, options, task, tasks[0])));
175
+ let newTasks = await Promise.all(urls.map(url => createTask(url, options, task, task.rootTaskURL || task.url)));
166
176
  newTasks = newTasks.filter(task => task &&
167
177
  testMaxDepth(task) &&
168
178
  !tasks.find(otherTask => otherTask.url == task.url) &&
179
+ !newTasks.find(otherTask => otherTask != task && otherTask.url == task.url) &&
169
180
  (!options.crawlInnerLinksOnly || task.isInnerLink) &&
170
181
  (!options.crawlNoParent || (task.isChild || !task.isInnerLink)));
171
182
  tasks.splice(tasks.length, 0, ...newTasks);
@@ -182,7 +193,7 @@ function testMaxDepth(task) {
182
193
  (options.crawlExternalLinksMaxDepth == 0 || task.externalLinkDepth < options.crawlExternalLinksMaxDepth);
183
194
  }
184
195
 
185
- async function createTask(url, options, parentTask, rootTask) {
196
+ async function createTask(url, options, parentTask, rootTaskURL) {
186
197
  url = parentTask ? rewriteURL(url, options.crawlRemoveURLFragment, options.crawlRewriteRules) : url;
187
198
  if (url) {
188
199
  if (!VALID_URL_TEST.test(url)) {
@@ -195,15 +206,15 @@ async function createTask(url, options, parentTask, rootTask) {
195
206
  throw new Error("Invalid URL or file path: " + url, { cause: error });
196
207
  }
197
208
  }
198
- const isInnerLink = rootTask && url.startsWith(getHostURL(rootTask.url));
199
- const rootBaseURIMatch = rootTask && rootTask.url.match(/(.*?)[^/]*$/);
209
+ const isInnerLink = rootTaskURL && url.startsWith(getHostURL(rootTaskURL));
210
+ const rootBaseURIMatch = rootTaskURL && rootTaskURL.match(/(.*?)[^/]*$/);
200
211
  const isChild = isInnerLink && rootBaseURIMatch && rootBaseURIMatch[1] && url.startsWith(rootBaseURIMatch[1]);
201
212
  return {
202
213
  url,
203
214
  isInnerLink,
204
215
  isChild,
205
216
  originalUrl: url,
206
- rootBaseURI: rootBaseURIMatch && rootBaseURIMatch[1],
217
+ rootTaskURL,
207
218
  depth: parentTask ? parentTask.depth + 1 : 0,
208
219
  externalLinkDepth: isInnerLink ? -1 : parentTask ? parentTask.externalLinkDepth + 1 : -1,
209
220
  options
@@ -282,7 +293,7 @@ async function capturePage(options) {
282
293
  if (options.outputJson) {
283
294
  filename += filename.endsWith(".json") ? "" : ".json";
284
295
  }
285
- const directoryName = await path.dirname(filename);
296
+ const directoryName = path.dirname(filename);
286
297
  if (directoryName !== ".") {
287
298
  await mkdir(directoryName, { recursive: true });
288
299
  }
@@ -338,7 +349,7 @@ async function getFilename(filename, options, index = 1) {
338
349
  if (options.filenameConflictAction != "skip") {
339
350
  return getFilename(filename, options, index + 1);
340
351
  }
341
- } catch (_) {
352
+ } catch {
342
353
  return newFilename;
343
354
  }
344
355
  }
@@ -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);
186
+ return [url, options];
187
+ } else {
188
+ return value;
189
+ }
190
+ });
191
+ return urls;
192
+ }