single-file-core 1.1.78 → 1.2.0

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.
@@ -0,0 +1,34 @@
1
+ /*
2
+ * Copyright 2010-2022 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ import { getProcessorHelperClass as getHelperInlineClass, cssTree } from "./lib/processor-helper-inline.js";
25
+ import { getProcessorHelperClass as getHelperClass } from "./lib/processor-helper.js";
26
+
27
+ export {
28
+ getProcessorHelperClass,
29
+ cssTree
30
+ };
31
+
32
+ function getProcessorHelperClass(options, utilInstance) {
33
+ return options.compressContent ? getHelperClass(utilInstance) : getHelperInlineClass(utilInstance);
34
+ }
package/core/util.js CHANGED
@@ -32,6 +32,13 @@ const ONE_MB = 1024 * 1024;
32
32
  const PREFIX_CONTENT_TYPE_TEXT = "text/";
33
33
  const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "\\\\", "?", "%", "*", ":", "|", "\"", "<", ">", "\x00-\x1f", "\x7F"];
34
34
  const DEFAULT_REPLACEMENT_CHARACTER = "_";
35
+ const CONTENT_TYPE_EXTENSIONS = {
36
+ "image/svg+xml": ".svg",
37
+ "image/png": ".png",
38
+ "image/jpeg": ".jpg",
39
+ "image/gif": ".gif",
40
+ "image/webp": ".webp"
41
+ };
35
42
 
36
43
  const URL = globalThis.URL;
37
44
  const DOMParser = globalThis.DOMParser;
@@ -51,6 +58,14 @@ function getInstance(utilOptions) {
51
58
  utilOptions.fetch = utilOptions.fetch || fetch;
52
59
  utilOptions.frameFetch = utilOptions.frameFetch || utilOptions.fetch || fetch;
53
60
  return {
61
+ getDoctypeString,
62
+ getFilenameExtension(resourceURL, replacedCharacters, replacementCharacter) {
63
+ const matchExtension = new URL(resourceURL).pathname.match(/(\.[^\\/.]*)$/);
64
+ return ((matchExtension && matchExtension[1] && this.getValidFilename(matchExtension[1], replacedCharacters, replacementCharacter)) || "").toLowerCase();
65
+ },
66
+ getContentTypeExtension(contentType) {
67
+ return CONTENT_TYPE_EXTENSIONS[contentType] || "";
68
+ },
54
69
  getContent,
55
70
  parseURL(resourceURL, baseURI) {
56
71
  if (baseURI === undefined) {
@@ -130,9 +145,6 @@ function getInstance(utilOptions) {
130
145
  removeUnusedFonts(doc, stylesheets, styles, options) {
131
146
  return modules.fontsMinifier.process(doc, stylesheets, styles, options);
132
147
  },
133
- removeAlternativeFonts(doc, stylesheets, fontDeclarations, fontTests) {
134
- return modules.fontsAltMinifier.process(doc, stylesheets, fontDeclarations, fontTests);
135
- },
136
148
  getMediaAllInfo(doc, stylesheets, styles) {
137
149
  return modules.matchedRules.getMediaAllInfo(doc, stylesheets, styles);
138
150
  },
@@ -185,7 +197,8 @@ function getInstance(utilOptions) {
185
197
  SINGLE_FILE_UI_ELEMENT_CLASS: helper.SINGLE_FILE_UI_ELEMENT_CLASS,
186
198
  EMPTY_RESOURCE: helper.EMPTY_RESOURCE,
187
199
  INFOBAR_TAGNAME: helper.INFOBAR_TAGNAME,
188
- WAIT_FOR_USERSCRIPT_PROPERTY_NAME: helper.WAIT_FOR_USERSCRIPT_PROPERTY_NAME
200
+ WAIT_FOR_USERSCRIPT_PROPERTY_NAME: helper.WAIT_FOR_USERSCRIPT_PROPERTY_NAME,
201
+ NO_SCRIPT_PROPERTY_NAME: helper.NO_SCRIPT_PROPERTY_NAME
189
202
  };
190
203
 
191
204
  async function getContent(resourceURL, options) {
@@ -241,7 +254,7 @@ function getInstance(utilOptions) {
241
254
  try {
242
255
  buffer = await response.arrayBuffer();
243
256
  } catch (error) {
244
- return { data: options.asBinary ? helper.EMPTY_RESOURCE : "", resourceURL };
257
+ return options.inline ? { data: options.asBinary ? helper.EMPTY_RESOURCE : "", resourceURL } : { resourceURL };
245
258
  }
246
259
  resourceURL = response.url || resourceURL;
247
260
  let contentType = "", charset;
@@ -300,12 +313,16 @@ function getInstance(utilOptions) {
300
313
  async function getFetchResponse(resourceURL, options, data, charset, contentType) {
301
314
  if (data) {
302
315
  if (options.asBinary) {
303
- const reader = new FileReader();
304
- reader.readAsDataURL(new Blob([data], { type: contentType + (options.charset ? ";charset=" + options.charset : "") }));
305
- data = await new Promise((resolve, reject) => {
306
- reader.addEventListener("load", () => resolve(reader.result), false);
307
- reader.addEventListener("error", reject, false);
308
- });
316
+ if (options.inline) {
317
+ const reader = new FileReader();
318
+ reader.readAsDataURL(new Blob([data], { type: contentType + (options.charset ? ";charset=" + options.charset : "") }));
319
+ data = await new Promise((resolve, reject) => {
320
+ reader.addEventListener("load", () => resolve(reader.result), false);
321
+ reader.addEventListener("error", reject, false);
322
+ });
323
+ } else {
324
+ data = new Uint8Array(data);
325
+ }
309
326
  } else {
310
327
  const firstBytes = new Uint8Array(data.slice(0, 4));
311
328
  if (firstBytes[0] == 132 && firstBytes[1] == 49 && firstBytes[2] == 149 && firstBytes[3] == 51) {
@@ -323,10 +340,10 @@ async function getFetchResponse(resourceURL, options, data, charset, contentType
323
340
  }
324
341
  data = data.replace(/\ufeff/gi, "");
325
342
  }
326
- } else {
343
+ } else if (options.inline) {
327
344
  data = options.asBinary ? helper.EMPTY_RESOURCE : "";
328
345
  }
329
- return { data, resourceURL, charset };
346
+ return { data, resourceURL, charset, contentType };
330
347
  }
331
348
 
332
349
  function guessMIMEType(expectedType, buffer) {
@@ -404,6 +421,24 @@ function hex(buffer) {
404
421
  return hexCodes.join("");
405
422
  }
406
423
 
424
+ function getDoctypeString(doc) {
425
+ const docType = doc.doctype;
426
+ let docTypeString = "";
427
+ if (docType) {
428
+ docTypeString = "<!DOCTYPE " + docType.nodeName;
429
+ if (docType.publicId) {
430
+ docTypeString += " PUBLIC \"" + docType.publicId + "\"";
431
+ if (docType.systemId)
432
+ docTypeString += " \"" + docType.systemId + "\"";
433
+ } else if (docType.systemId)
434
+ docTypeString += " SYSTEM \"" + docType.systemId + "\"";
435
+ if (docType.internalSubset)
436
+ docTypeString += " [" + docType.internalSubset + "]";
437
+ docTypeString += "> ";
438
+ }
439
+ return docTypeString;
440
+ }
441
+
407
442
  function log(...args) {
408
443
  console.log("S-File <browser>", ...args); // eslint-disable-line no-console
409
444
  }
@@ -218,27 +218,13 @@ function getFontFamilyNames(declarations) {
218
218
  let fontFamilyName = declarations.children.filter(node => node.property == "font-family").tail;
219
219
  let fontFamilyNames = [];
220
220
  if (fontFamilyName) {
221
- let familyName = "";
222
221
  if (fontFamilyName.data.value.children) {
223
- let previousType;
224
- fontFamilyName.data.value.children.forEach(node => {
225
- if (node.type == "Operator" && node.value == "," && familyName) {
226
- fontFamilyNames.push(helper.normalizeFontFamily(familyName));
227
- familyName = "";
228
- previousType = null;
229
- } else {
230
- if (previousType == "Identifier" && node.type == "Identifier") {
231
- familyName += " ";
232
- }
233
- familyName += cssTree.generate(node);
234
- }
235
- previousType = node.type;
236
- });
222
+ parseFamilyNames(fontFamilyName.data.value, fontFamilyNames);
237
223
  } else {
238
224
  fontFamilyName = cssTree.generate(fontFamilyName.data.value);
239
- }
240
- if (familyName) {
241
- fontFamilyNames.push(helper.normalizeFontFamily(familyName));
225
+ if (fontFamilyName) {
226
+ fontFamilyNames.push(helper.normalizeFontFamily(fontFamilyName));
227
+ }
242
228
  }
243
229
  }
244
230
  const font = declarations.children.filter(node => node.property == "font").tail;
@@ -253,6 +239,43 @@ function getFontFamilyNames(declarations) {
253
239
  return fontFamilyNames;
254
240
  }
255
241
 
242
+ function parseFamilyNames(fontFamilyNameTokenData, fontFamilyNames) {
243
+ let nextToken = fontFamilyNameTokenData.children.head;
244
+ while (nextToken) {
245
+ if (nextToken.data.type == "Identifier") {
246
+ let familyName = nextToken.data.name;
247
+ let nextIdentifierToken = nextToken.next;
248
+ while (nextIdentifierToken && nextIdentifierToken.data.type != "Operator" && nextIdentifierToken.data.value != ",") {
249
+ familyName += " " + nextIdentifierToken.data.name;
250
+ nextIdentifierToken = nextIdentifierToken.next;
251
+ }
252
+ fontFamilyNames.push(helper.normalizeFontFamily(familyName));
253
+ nextToken = nextToken.next;
254
+ } else if (nextToken.data.type == "Function" && nextToken.data.name == "var" && nextToken.data.children) {
255
+ const varName = nextToken.data.children.head.data.name;
256
+ fontFamilyNames.push(helper.normalizeFontFamily("var(" + varName + ")"));
257
+ let nextValueToken = nextToken.data.children.head.next;
258
+ while (nextValueToken && nextValueToken.data.type == "Operator" && nextValueToken.data.value == ",") {
259
+ nextValueToken = nextValueToken.next;
260
+ }
261
+ const fallbackToken = nextValueToken;
262
+ if (fallbackToken) {
263
+ if (fallbackToken.data.children) {
264
+ parseFamilyNames(fallbackToken.data, fontFamilyNames);
265
+ } else {
266
+ fontFamilyNames.push(helper.normalizeFontFamily(fallbackToken.data.value));
267
+ }
268
+ }
269
+ nextToken = nextToken.next;
270
+ } else if (nextToken.data.type == "String") {
271
+ fontFamilyNames.push(helper.normalizeFontFamily(nextToken.data.value));
272
+ nextToken = nextToken.next;
273
+ } else if (nextToken.data.type == "Operator" && nextToken.data.value == ",") {
274
+ nextToken = nextToken.next;
275
+ }
276
+ }
277
+ }
278
+
256
279
  function getUsedFontWeight(fontInfo, fontStyle, fontWeights) {
257
280
  let foundWeight;
258
281
  fontWeights = fontWeights.map(weight => String(Number.parseInt(weight, 10)));
package/modules/index.js CHANGED
@@ -21,7 +21,6 @@
21
21
  * Source.
22
22
  */
23
23
 
24
- import * as fontsAltMinifier from "./css-fonts-alt-minifier.js";
25
24
  import * as fontsMinifier from "./css-fonts-minifier.js";
26
25
  import * as matchedRules from "./css-matched-rules.js";
27
26
  import * as mediasAltMinifier from "./css-medias-alt-minifier.js";
@@ -32,7 +31,6 @@ import * as serializer from "./html-serializer.js";
32
31
  import * as templateFormatter from "./template-formatter.js";
33
32
 
34
33
  export {
35
- fontsAltMinifier,
36
34
  fontsMinifier,
37
35
  matchedRules,
38
36
  mediasAltMinifier,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.1.78",
3
+ "version": "1.2.0",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Copyright 2010-2022 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global DOMParser */
25
+
26
+ export {
27
+ display
28
+ };
29
+
30
+ async function display(document, docContent, { disableFramePointerEvents } = {}) {
31
+ const DISABLED_NOSCRIPT_ATTRIBUTE_NAME = "data-single-filez-disabled-noscript";
32
+ const doc = (new DOMParser()).parseFromString(docContent, "text/html");
33
+ doc.querySelectorAll("noscript:not([" + DISABLED_NOSCRIPT_ATTRIBUTE_NAME + "])").forEach(element => {
34
+ element.setAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME, element.innerHTML);
35
+ element.textContent = "";
36
+ });
37
+ if (doc.doctype) {
38
+ if (document.doctype) {
39
+ document.replaceChild(doc.doctype, document.doctype);
40
+ } else {
41
+ document.insertBefore(doc.doctype, document.documentElement);
42
+ }
43
+ } else if (document.doctype) {
44
+ document.doctype.remove();
45
+ }
46
+ if (disableFramePointerEvents) {
47
+ doc.querySelectorAll("iframe").forEach(element => {
48
+ const pointerEvents = "pointer-events";
49
+ element.style.setProperty("-sf-" + pointerEvents, element.style.getPropertyValue(pointerEvents), element.style.getPropertyPriority(pointerEvents));
50
+ element.style.setProperty(pointerEvents, "none", "important");
51
+ });
52
+ }
53
+ document.replaceChild(document.importNode(doc.documentElement, true), document.documentElement);
54
+ document.documentElement.setAttribute("data-sfz", "");
55
+ document.querySelectorAll("link[rel*=icon]").forEach(element => element.parentElement.replaceChild(element, element));
56
+ document.open = document.write = document.close = () => { };
57
+ for (let element of Array.from(document.querySelectorAll("script"))) {
58
+ await new Promise((resolve, reject) => {
59
+ const scriptElement = document.createElement("script");
60
+ Array.from(element.attributes).forEach(attribute => scriptElement.setAttribute(attribute.name, attribute.value));
61
+ const async = element.getAttribute("async") == "" || element.getAttribute("async") == "async";
62
+ if (element.textContent) {
63
+ scriptElement.textContent = element.textContent;
64
+ } else if (!async) {
65
+ scriptElement.addEventListener("load", resolve);
66
+ scriptElement.addEventListener("error", reject);
67
+ }
68
+ element.parentElement.replaceChild(scriptElement, element);
69
+ if (element.textContent || async) {
70
+ resolve();
71
+ }
72
+ });
73
+ }
74
+ }
@@ -0,0 +1,151 @@
1
+ /*
2
+ * Copyright 2010-2022 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global zip, Blob, FileReader, URL */
25
+
26
+ export {
27
+ extract
28
+ };
29
+
30
+ async function extract(content, { password, prompt = () => { }, shadowRootScriptURL, zipOptions = { useWebWorkers: false }, noBlobURL } = {}) {
31
+ const KNOWN_MIMETYPES = {
32
+ "gif": "image/gif",
33
+ "jpg": "image/jpeg",
34
+ "png": "image/png",
35
+ "tif": "image/tiff",
36
+ "tiff": "image/tiff",
37
+ "bmp": "image/bmp",
38
+ "ico": "image/vnd.microsoft.icon",
39
+ "webp": "image/webp",
40
+ "svg": "image/svg+xml",
41
+ "avi": "video/x-msvideo",
42
+ "ogv": "video/ogg",
43
+ "mp4": "video/mp4",
44
+ "mpeg": "video/mpeg",
45
+ "ts": "video/mp2t",
46
+ "webm": "video/webm",
47
+ "3gp": "video/3gpp",
48
+ "3g2": "video/3gpp",
49
+ "mp3": "audio/mpeg",
50
+ "oga": "audio/ogg",
51
+ "mid": "audio/midi",
52
+ "midi": "audio/midi",
53
+ "opus": "audio/opus",
54
+ "wav": "audio/wav",
55
+ "weba": "audio/webm"
56
+ };
57
+ if (Array.isArray(content)) {
58
+ content = new Blob([new Uint8Array(content)]);
59
+ }
60
+ zip.configure(zipOptions);
61
+ const blobReader = new zip.BlobReader(content);
62
+ let resources = [];
63
+ const zipReader = new zip.ZipReader(blobReader);
64
+ const entries = await zipReader.getEntries();
65
+ const options = { password };
66
+ await Promise.all(entries.map(async entry => {
67
+ let dataWriter, content, textContent, name, blob;
68
+ if (!options.password && entry.encrypted) {
69
+ options.password = prompt("Please enter the password to view the page");
70
+ }
71
+ name = entry.filename.match(/^([0-9_]+\/)?(.*)$/)[2];
72
+ if (entry.filename.match(/index\.html$/) || entry.filename.match(/stylesheet_[0-9]+\.css/) || entry.filename.match(/scripts\/[0-9]+\.js/)) {
73
+ dataWriter = new zip.TextWriter();
74
+ textContent = await entry.getData(dataWriter, options);
75
+ } else {
76
+ const extension = entry.filename.match(/\.([^.]+)/);
77
+ let mimeType;
78
+ if (extension && extension[1] && KNOWN_MIMETYPES[extension[1]]) {
79
+ mimeType = KNOWN_MIMETYPES[extension[1]];
80
+ } else {
81
+ mimeType = "application/octet-stream";
82
+ }
83
+ if (entry.filename.match(/frames\//) || noBlobURL) {
84
+ content = await entry.getData(new zip.Data64URIWriter(mimeType), options);
85
+ } else {
86
+ blob = await entry.getData(new zip.BlobWriter(mimeType), options);
87
+ content = URL.createObjectURL(blob);
88
+ }
89
+ }
90
+ resources.push({ filename: entry.filename, name, url: entry.comment, content, blob, textContent, parentResources: [] });
91
+ }));
92
+ await zipReader.close();
93
+ let docContent, origDocContent, url;
94
+ resources = resources.sort((resourceLeft, resourceRight) => resourceRight.filename.length - resourceLeft.filename.length);
95
+ const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
96
+ for (const resource of resources) {
97
+ if (resource.textContent !== undefined) {
98
+ let prefixPath = "";
99
+ const prefixPathMatch = resource.filename.match(/(.*\/)[^/]+$/);
100
+ if (prefixPathMatch && prefixPathMatch[1]) {
101
+ prefixPath = prefixPathMatch[1];
102
+ }
103
+ if (resource.filename.match(/^([0-9_]+\/)?index\.html$/)) {
104
+ origDocContent = resource.textContent;
105
+ }
106
+ const isScript = resource.filename.match(/scripts\/[0-9]+\.js/);
107
+ if (!isScript) {
108
+ resources.forEach(innerResource => {
109
+ if (innerResource.filename.startsWith(prefixPath) && innerResource.filename != resource.filename) {
110
+ const filename = innerResource.filename.substring(prefixPath.length);
111
+ if (!filename.match(/manifest\.json$/)) {
112
+ const searchRegExp = new RegExp(filename.replace(REGEXP_ESCAPE, "\\$1"), "g");
113
+ const position = resource.textContent.search(searchRegExp);
114
+ if (position != -1) {
115
+ innerResource.parentResources.push(resource.filename);
116
+ resource.textContent = resource.textContent.replace(searchRegExp, innerResource.content);
117
+ }
118
+ }
119
+ }
120
+ });
121
+ }
122
+ let mimeType;
123
+ if (resource.filename.match(/stylesheet_[0-9]+\.css/)) {
124
+ mimeType = "text/css";
125
+ } else if (isScript) {
126
+ mimeType = "text/javascript";
127
+ } else if (resource.filename.match(/index\.html$/)) {
128
+ mimeType = "text/html";
129
+ if (shadowRootScriptURL) {
130
+ resource.textContent = resource.textContent.replace(/<script data-template-shadow-root.*<\/script>/g, "<script data-template-shadow-root src=" + shadowRootScriptURL + "></" + "script>");
131
+ }
132
+ }
133
+ if (resource.filename.match(/^([0-9_]+\/)?index\.html$/)) {
134
+ docContent = resource.textContent;
135
+ url = resource.url;
136
+ } else {
137
+ const reader = new FileReader();
138
+ if (resource.textContent) {
139
+ reader.readAsDataURL(new Blob([resource.textContent], { type: mimeType + ";charset=utf-8" }));
140
+ resource.content = await new Promise((resolve, reject) => {
141
+ reader.addEventListener("load", () => resolve(reader.result), false);
142
+ reader.addEventListener("error", reject, false);
143
+ });
144
+ } else {
145
+ resource.content = "data:text/plain,";
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return { docContent, origDocContent, resources, url };
151
+ }