|
|
@@ -99,7 +99,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}, {
|
|
|
sequential: [
|
|
|
{ option: "removeUnusedStyles", action: "removeUnusedStyles" },
|
|
|
- { option: "groupDuplicateImages", action: "groupDuplicateImages" },
|
|
|
{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
|
|
|
{ option: "removeAlternativeMedias", action: "removeAlternativeMedias" }
|
|
|
],
|
|
|
@@ -260,7 +259,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
const resourceContent = await Download.getContent(resourceURL, { asDataURI, maxResourceSize: options.maxResourceSize, maxResourceSizeEnabled: options.maxResourceSizeEnabled });
|
|
|
indexResource = indexResource + 1;
|
|
|
onloadListener({ index: indexResource, url: resourceURL });
|
|
|
- resourceRequests.forEach(resourceRequest => resourceRequest.resolve(resourceContent));
|
|
|
+ resourceRequests.forEach(resourceRequest => resourceRequest.resolve({ content: resourceContent, indexResource, duplicate: Boolean(resourceRequests.length > 1) }));
|
|
|
} catch (error) {
|
|
|
indexResource = indexResource + 1;
|
|
|
onloadListener({ index: indexResource, url: resourceURL });
|
|
|
@@ -578,12 +577,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- groupDuplicateImages() {
|
|
|
- if (this.options.imageData) {
|
|
|
- DOM.minifyImages(this.doc, this.options);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
removeAlternativeMedias() {
|
|
|
const stats = DOM.minifyMedias(this.doc);
|
|
|
this.stats.set("processed", "medias", stats.processed);
|
|
|
@@ -682,26 +675,26 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
|
|
|
async pageResources() {
|
|
|
const resourcePromises = [
|
|
|
- DomProcessorHelper.processAttribute(this.doc.querySelectorAll("link[href][rel*=\"icon\"]"), "href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
|
|
|
- DomProcessorHelper.processAttribute(this.doc.querySelectorAll("object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]"), "data", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.batchRequest),
|
|
|
- DomProcessorHelper.processAttribute(this.doc.querySelectorAll("img[src], input[src][type=image]"), "src", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
|
|
|
- DomProcessorHelper.processAttribute(this.doc.querySelectorAll("embed[src*=\".svg\"]"), "src", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.batchRequest),
|
|
|
- DomProcessorHelper.processAttribute(this.doc.querySelectorAll("video[poster]"), "poster", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
|
|
|
- DomProcessorHelper.processAttribute(this.doc.querySelectorAll("*[background]"), "background", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
|
|
|
- DomProcessorHelper.processAttribute(this.doc.querySelectorAll("image"), "xlink:href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
|
|
|
- DomProcessorHelper.processXLinks(this.doc.querySelectorAll("use"), this.baseURI, this.batchRequest),
|
|
|
- DomProcessorHelper.processSrcset(this.doc.querySelectorAll("img[srcset], source[srcset]"), "srcset", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)
|
|
|
+ DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("link[href][rel*=\"icon\"]"), "href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options),
|
|
|
+ DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]"), "data", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.batchRequest, this.options),
|
|
|
+ DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("img[src], input[src][type=image]"), "src", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options, true),
|
|
|
+ DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("embed[src*=\".svg\"]"), "src", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.batchRequest, this.options),
|
|
|
+ DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("video[poster]"), "poster", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options),
|
|
|
+ DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("*[background]"), "background", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options),
|
|
|
+ DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("image"), "xlink:href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options),
|
|
|
+ DomProcessorHelper.processXLinks(this.doc, this.doc.querySelectorAll("use"), this.baseURI, this.batchRequest),
|
|
|
+ DomProcessorHelper.processSrcset(this.doc, this.doc.querySelectorAll("img[srcset], source[srcset]"), "srcset", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)
|
|
|
];
|
|
|
if (!this.options.removeAudioSrc) {
|
|
|
- resourcePromises.push(DomProcessorHelper.processAttribute(this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", PREFIX_DATA_URI_AUDIO, this.baseURI, this.batchRequest));
|
|
|
+ resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", PREFIX_DATA_URI_AUDIO, this.baseURI, this.batchRequest, this.options));
|
|
|
}
|
|
|
if (!this.options.removeVideoSrc) {
|
|
|
- resourcePromises.push(DomProcessorHelper.processAttribute(this.doc.querySelectorAll("video[src], video > source[src]"), "src", PREFIX_DATA_URI_VIDEO, this.baseURI, this.batchRequest));
|
|
|
+ resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("video[src], video > source[src]"), "src", PREFIX_DATA_URI_VIDEO, this.baseURI, this.batchRequest, this.options));
|
|
|
}
|
|
|
if (this.options.lazyLoadImages) {
|
|
|
const imageSelectors = DOM.lazyLoaderImageSelectors();
|
|
|
- Object.keys(imageSelectors.src).forEach(selector => resourcePromises.push(DomProcessorHelper.processAttribute(this.doc.querySelectorAll(selector), imageSelectors.src[selector], PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)));
|
|
|
- Object.keys(imageSelectors.srcset).forEach(selector => resourcePromises.push(DomProcessorHelper.processSrcset(this.doc.querySelectorAll(selector), imageSelectors.srcset[selector], PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)));
|
|
|
+ Object.keys(imageSelectors.src).forEach(selector => resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll(selector), imageSelectors.src[selector], PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options)));
|
|
|
+ Object.keys(imageSelectors.srcset).forEach(selector => resourcePromises.push(DomProcessorHelper.processSrcset(this.doc, this.doc.querySelectorAll(selector), imageSelectors.srcset[selector], PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)));
|
|
|
}
|
|
|
await resourcePromises;
|
|
|
}
|
|
|
@@ -711,9 +704,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
|
|
|
|
async processStylesheets() {
|
|
|
- await Promise.all(Array.from(this.doc.querySelectorAll("style")).map(async (styleElement, indexStyle) => {
|
|
|
+ await Promise.all(Array.from(this.doc.querySelectorAll("style")).map(async styleElement => {
|
|
|
this.stats.add("processed", "stylesheets", 1);
|
|
|
- styleElement.textContent = await DomProcessorHelper.processStylesheet(styleElement.textContent, styleElement.sheet.cssRules, this.baseURI, this.options, false, indexStyle, this.batchRequest);
|
|
|
+ styleElement.textContent = await DomProcessorHelper.processStylesheet(this.doc, styleElement.textContent, styleElement.sheet.cssRules, this.baseURI, this.options, this.batchRequest);
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
@@ -835,7 +828,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
|
|
|
|
async processStyleAttributes() {
|
|
|
- await Promise.all(Array.from(this.doc.querySelectorAll("[style]")).map(async element => element.setAttribute("style", await DomProcessorHelper.processStylesheet(element.getAttribute("style"), [{ type: CSSRule.STYLE_RULE, cssText: element.getAttribute("style") }], this.baseURI, this.options, true, 0, this.batchRequest))));
|
|
|
+ await Promise.all(Array.from(this.doc.querySelectorAll("[style]")).map(async element => element.setAttribute("style", await DomProcessorHelper.processStylesheet(this.doc, element.getAttribute("style"), [{ type: CSSRule.STYLE_RULE, cssText: element.getAttribute("style") }], this.baseURI, this.options, this.batchRequest))));
|
|
|
}
|
|
|
|
|
|
async resolveLinkedStylesheetURLs() {
|
|
|
@@ -863,6 +856,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
|
|
|
const PREFIX_DATA_URI_NO_MIMETYPE = "data:;";
|
|
|
const PREFIX_DATA_URI_OCTET_STREAM = "data:application/octet-stream";
|
|
|
+ const SINGLE_FILE_VARIABLE_NAME_PREFIX = "--sf-img-";
|
|
|
|
|
|
class DomProcessorHelper {
|
|
|
static async getFilename(options, content) {
|
|
|
@@ -1016,22 +1010,22 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- static async processStylesheet(stylesheetContent, cssRules, baseURI, options, inline, indexStyle, batchRequest) {
|
|
|
+ static async processStylesheet(doc, stylesheetContent, cssRules, baseURI, options, batchRequest) {
|
|
|
let sheetContent = "", variablesInfo = { index: 0, cssText: "" };
|
|
|
const urlFunctions = DomUtil.getUrlFunctions(stylesheetContent);
|
|
|
- const variableNames = new Map();
|
|
|
const urlFunctionData = new Map();
|
|
|
await Promise.all(urlFunctions.map(async urlFunction => {
|
|
|
const originalResourceURL = DomUtil.matchURL(urlFunction);
|
|
|
const resourceURL = DomUtil.normalizeURL(originalResourceURL);
|
|
|
if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL) && stylesheetContent.includes(urlFunction)) {
|
|
|
- const dataURI = await batchRequest.addURL(resourceURL);
|
|
|
+ const { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL);
|
|
|
const regExpUrlFunction = DomUtil.getRegExp(urlFunction);
|
|
|
- const functions = stylesheetContent.match(regExpUrlFunction);
|
|
|
- if (options.groupDuplicateImages && functions.length > 1) {
|
|
|
- variableNames.set(urlFunction, "--single-file-" + indexStyle + "-" + variableNames.size);
|
|
|
+ if (duplicate && options.groupDuplicateImages) {
|
|
|
+ urlFunctionData.set(urlFunction, { regExpUrlFunction, dataURI: content, variableName: "var(" + SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource + ")" });
|
|
|
+ DomUtil.insertVariable(doc, indexResource, content, options);
|
|
|
+ } else {
|
|
|
+ urlFunctionData.set(urlFunction, { regExpUrlFunction, dataURI: content });
|
|
|
}
|
|
|
- urlFunctionData.set(urlFunction, { regExpUrlFunction, dataURI });
|
|
|
}
|
|
|
}));
|
|
|
const rulesContent = processRules(cssRules);
|
|
|
@@ -1047,7 +1041,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
const mediaRulesContent = processRules(cssRule.cssRules);
|
|
|
rulesContent += "@media " + Array.from(cssRule.media).join(",") + "{" + mediaRulesContent + "}";
|
|
|
} else if (cssRule.type == CSSRule.STYLE_RULE) {
|
|
|
- rulesContent += processURLFunctions(cssRule.cssText, !inline && options.compressCSS);
|
|
|
+ rulesContent += processURLFunctions(cssRule.cssText);
|
|
|
} else {
|
|
|
rulesContent += processURLFunctions(cssRule.cssText);
|
|
|
}
|
|
|
@@ -1055,28 +1049,17 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
return rulesContent;
|
|
|
}
|
|
|
|
|
|
- function processURLFunctions(cssText, useCustomProperties) {
|
|
|
+ function processURLFunctions(cssText) {
|
|
|
const urlFunctions = DomUtil.getUrlFunctions(cssText);
|
|
|
urlFunctions.forEach(urlFunction => {
|
|
|
const originalResourceURL = DomUtil.matchURL(urlFunction);
|
|
|
const resourceURL = DomUtil.normalizeURL(originalResourceURL);
|
|
|
if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL) && cssText.includes(urlFunction)) {
|
|
|
const resourceInfo = urlFunctionData.get(urlFunction);
|
|
|
- const dataURI = resourceInfo.dataURI;
|
|
|
- if (useCustomProperties) {
|
|
|
- const variableName = variableNames.get(urlFunction);
|
|
|
- if (variableName) {
|
|
|
- cssText = cssText.replace(resourceInfo.regExpUrlFunction, "var(" + variableName + ")");
|
|
|
- if (variablesInfo.cssText) {
|
|
|
- variablesInfo.cssText += ";";
|
|
|
- }
|
|
|
- variablesInfo.cssText += variableName + ":url(\"" + dataURI + "\")";
|
|
|
- variablesInfo.index++;
|
|
|
- } else {
|
|
|
- cssText = cssText.replace(resourceInfo.regExpUrlFunction, urlFunction.replace(originalResourceURL, dataURI));
|
|
|
- }
|
|
|
+ if (options.groupDuplicateImages && resourceInfo.variableName) {
|
|
|
+ cssText = cssText.replace(resourceInfo.regExpUrlFunction, resourceInfo.variableName);
|
|
|
} else {
|
|
|
- cssText = cssText.replace(resourceInfo.regExpUrlFunction, urlFunction.replace(originalResourceURL, dataURI));
|
|
|
+ cssText = cssText.replace(resourceInfo.regExpUrlFunction, urlFunction.replace(originalResourceURL, resourceInfo.dataURI));
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
@@ -1084,16 +1067,25 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- static async processAttribute(resourceElements, attributeName, prefixDataURI, baseURI, batchRequest) {
|
|
|
+ static async processAttribute(doc, resourceElements, attributeName, prefixDataURI, baseURI, batchRequest, options, processDuplicates) {
|
|
|
await Promise.all(Array.from(resourceElements).map(async resourceElement => {
|
|
|
let resourceURL = resourceElement.getAttribute(attributeName);
|
|
|
if (resourceURL) {
|
|
|
resourceURL = DomUtil.normalizeURL(resourceURL);
|
|
|
if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
|
|
|
try {
|
|
|
- const dataURI = await batchRequest.addURL(new URL(resourceURL, baseURI).href);
|
|
|
- if (dataURI.startsWith(prefixDataURI) || dataURI.startsWith(PREFIX_DATA_URI_NO_MIMETYPE) || dataURI.startsWith(PREFIX_DATA_URI_OCTET_STREAM)) {
|
|
|
- resourceElement.setAttribute(attributeName, dataURI);
|
|
|
+ const { content, indexResource, duplicate } = await batchRequest.addURL(new URL(resourceURL, baseURI).href);
|
|
|
+ if (content.startsWith(prefixDataURI) || content.startsWith(PREFIX_DATA_URI_NO_MIMETYPE) || content.startsWith(PREFIX_DATA_URI_OCTET_STREAM)) {
|
|
|
+ if (processDuplicates && duplicate && options.groupDuplicateImages) {
|
|
|
+ if (resourceElement.style.getPropertyValue("background-image")) {
|
|
|
+ resourceElement.setAttribute(attributeName, content);
|
|
|
+ } else {
|
|
|
+ DomUtil.replaceImageSource(resourceElement, SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource, options);
|
|
|
+ DomUtil.insertVariable(doc, indexResource, content, options);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ resourceElement.setAttribute(attributeName, content);
|
|
|
+ }
|
|
|
} else {
|
|
|
resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
|
|
|
}
|
|
|
@@ -1105,7 +1097,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
- static async processXLinks(resourceElements, baseURI, batchRequest) {
|
|
|
+ static async processXLinks(doc, resourceElements, baseURI, batchRequest) {
|
|
|
const attributeName = "xlink:href";
|
|
|
await Promise.all(Array.from(resourceElements).map(async resourceElement => {
|
|
|
const originalResourceURL = resourceElement.getAttribute(attributeName);
|
|
|
@@ -1113,7 +1105,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
const resourceURL = DomUtil.normalizeURL(originalResourceURL);
|
|
|
if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
|
|
|
try {
|
|
|
- const content = await batchRequest.addURL(new URL(resourceURL, baseURI).href, false);
|
|
|
+ const { content } = await batchRequest.addURL(new URL(resourceURL, baseURI).href, false);
|
|
|
const DOMParser = DOM.getParser();
|
|
|
if (DOMParser) {
|
|
|
const svgDoc = new DOMParser().parseFromString(content, "image/svg+xml");
|
|
|
@@ -1138,18 +1130,18 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
- static async processSrcset(resourceElements, attributeName, prefixDataURI, baseURI, batchRequest) {
|
|
|
+ static async processSrcset(doc, resourceElements, attributeName, prefixDataURI, baseURI, batchRequest) {
|
|
|
await Promise.all(Array.from(resourceElements).map(async resourceElement => {
|
|
|
const srcset = DOM.parseSrcset(resourceElement.getAttribute(attributeName));
|
|
|
const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
|
|
|
const resourceURL = DomUtil.normalizeURL(srcsetValue.url);
|
|
|
if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
|
|
|
try {
|
|
|
- let dataURI = await batchRequest.addURL(new URL(resourceURL, baseURI).href);
|
|
|
- if (!dataURI.startsWith(prefixDataURI) && !dataURI.startsWith(PREFIX_DATA_URI_NO_MIMETYPE) && !dataURI.startsWith(PREFIX_DATA_URI_OCTET_STREAM)) {
|
|
|
+ const { content } = await batchRequest.addURL(new URL(resourceURL, baseURI).href);
|
|
|
+ if (!content.startsWith(prefixDataURI) && !content.startsWith(PREFIX_DATA_URI_NO_MIMETYPE) && !content.startsWith(PREFIX_DATA_URI_OCTET_STREAM)) {
|
|
|
resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
|
|
|
}
|
|
|
- return dataURI + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
|
|
|
+ return content + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
|
|
|
} catch (error) {
|
|
|
resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
|
|
|
}
|
|
|
@@ -1277,6 +1269,41 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ static insertVariable(doc, indexResource, content, options) {
|
|
|
+ const sheetAttributeName = DOM.sheetAttributeName(options.sessionId);
|
|
|
+ let styleElement = doc.querySelector("style[" + sheetAttributeName + "]"), insertedVariables;
|
|
|
+ if (!styleElement) {
|
|
|
+ styleElement = doc.createElement("style");
|
|
|
+ if (doc.head.firstChild) {
|
|
|
+ doc.head.insertBefore(styleElement, doc.head.firstChild);
|
|
|
+ } else {
|
|
|
+ doc.head.appendChild(styleElement);
|
|
|
+ }
|
|
|
+ styleElement.setAttribute(sheetAttributeName, "[]");
|
|
|
+ insertedVariables = [];
|
|
|
+ } else {
|
|
|
+ insertedVariables = JSON.parse(styleElement.getAttribute(sheetAttributeName));
|
|
|
+ }
|
|
|
+ if (!insertedVariables.includes(indexResource)) {
|
|
|
+ insertedVariables.push(indexResource);
|
|
|
+ styleElement.textContent = styleElement.textContent + `:root{${SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource}:url("${content}")}`;
|
|
|
+ styleElement.setAttribute(sheetAttributeName, JSON.stringify(insertedVariables));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static replaceImageSource(imgElement, variableName, options) {
|
|
|
+ const dataAttributeName = DOM.imagesAttributeName(options.sessionId);
|
|
|
+ if (imgElement.getAttribute(dataAttributeName) != null) {
|
|
|
+ const imgData = options.imageData[Number(imgElement.getAttribute(dataAttributeName))];
|
|
|
+ imgElement.setAttribute("src", `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="${imgData.width}" height="${imgData.height}"><rect fill-opacity="0"/></svg>`);
|
|
|
+ imgElement.style.setProperty("background-image", "var(" + variableName + ")", "important");
|
|
|
+ imgElement.style.setProperty("background-size", `${imgData.width}px ${imgData.height}px`, "important");
|
|
|
+ imgElement.style.setProperty("background-origin", "content-box", "important");
|
|
|
+ imgElement.style.setProperty("background-repeat", "no-repeat", "important");
|
|
|
+ imgElement.removeAttribute(dataAttributeName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|
|
|
function log(...args) {
|