@bradymholt/ampify
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -1,2 +0,2 @@ | ||
declare module "ampify" { | ||
declare module "@bradymholt/ampify" { | ||
export = ampify; | ||
@@ -3,0 +3,0 @@ } |
295
index.js
@@ -1,64 +0,116 @@ | ||
const fs = require('fs'); | ||
const url = require('url'); | ||
const axios = require('axios'); | ||
const cheerio = require('cheerio'); | ||
const sizeOf = require('image-size'); | ||
const CleanCss = require('clean-css'); | ||
const fs = require("fs"); | ||
const url = require("url"); | ||
const axios = require("axios"); | ||
const cheerio = require("cheerio"); | ||
const sizeOf = require("image-size"); | ||
const CleanCss = require("clean-css"); | ||
const pretty = require('pretty'); | ||
module.exports = async (html, options) => { | ||
const tags = { | ||
amp: ['img', 'video'], | ||
}; | ||
let youtube = false; | ||
const cheerioOptions = options || { | ||
cwd: options.cwd || '', | ||
cwd: options.cwd || "", | ||
round: options.round || true, | ||
normalizeWhitespace: options.normalizeWhitespace || false, | ||
xmlMode: options.xmlMode || false, | ||
decodeEntities: options.decodeEntities || false, | ||
decodeEntities: options.decodeEntities || false | ||
}; | ||
const round = cheerioOptions.round | ||
? numb => Math.round(numb / 5) * 5 | ||
: numb => numb; | ||
// Load html | ||
const $ = cheerio.load(html, cheerioOptions); | ||
const round = cheerioOptions.round ? numb => Math.round(numb / 5) * 5 : numb => numb; | ||
const headElement = $("head"); | ||
/* AMP Boilerplate */ | ||
// add amp atrribute to <html/> element | ||
$("html") | ||
.first() | ||
.attr("amp", ""); | ||
// set charset to utf-8 | ||
headElement.find("meta[charset]").remove(); | ||
headElement.prepend('<meta charset="utf-8">'); | ||
/* meta viewport */ | ||
headElement.find("meta[name='viewport']").remove(); | ||
headElement.append( | ||
'<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">' | ||
); | ||
/* amp-boilerplate styles */ | ||
if (headElement.find("style[amp-boilerplate]").length === 0) { | ||
headElement.append( | ||
'<style amp-boilerplate="">body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>' | ||
); | ||
} | ||
// AMP script | ||
headElement.find("script[src*='cdn.ampproject.org']").remove(); | ||
headElement.append( | ||
'<script async src="https://cdn.ampproject.org/v0.js"></script>' | ||
); | ||
/* remove non-AMP scripts */ | ||
$("script").each((index, element) => { | ||
const el = $(element); | ||
if ( | ||
el.attr("custom-element") == "amp-analytics" || | ||
el.attr("src") && el.attr("src").indexOf("cdn.ampproject.org") > -1 | ||
) { | ||
return; | ||
} else { | ||
el.remove(); | ||
} | ||
}); | ||
/* Fetch images and CSS */ | ||
const promises = []; | ||
const responses = {}; | ||
const externalSrcContent = {}; | ||
$('img:not([width]):not([height])').each((index, element) => { | ||
const src = $(element).attr('src'); | ||
// Fetch external images | ||
$("img:not([width]):not([height])").each((index, element) => { | ||
const src = $(element).attr("src"); | ||
// skip if already fetched | ||
if (responses[src]) { | ||
if (externalSrcContent[src]) { | ||
return; | ||
} | ||
if (src && src.indexOf('//') !== -1) { | ||
if (src && src.indexOf("//") !== -1) { | ||
// set a flag | ||
responses[src] = true; | ||
externalSrcContent[src] = true; | ||
const imageUrl = element.attribs.src; | ||
promises.push(axios.get(imageUrl, { responseType: 'arraybuffer' }) | ||
.then((response) => { | ||
responses[src] = response; | ||
})); | ||
promises.push( | ||
axios.get(imageUrl, { responseType: "arraybuffer" }).then(response => { | ||
externalSrcContent[src] = response; | ||
}) | ||
); | ||
} | ||
}); | ||
$('link[rel=stylesheet]').each((index, element) => { | ||
const src = $(element).attr('href'); | ||
if (responses[src]) { | ||
// Fetch external CSS | ||
$("link[rel=stylesheet]").each((index, element) => { | ||
const src = $(element).attr("href"); | ||
if (isGoogleFontHostSrc(src)) { | ||
// External links to fonts are allowed; we'll make a specific exception for Google Fonts | ||
return; | ||
} | ||
if (externalSrcContent[src]) { | ||
// We've already cached this one | ||
return; | ||
} | ||
try { | ||
if (src && src.indexOf('//') !== -1) { | ||
if (src && src.indexOf("//") !== -1) { | ||
let cssSrc = src; | ||
if (src.indexOf('//') === 0) { | ||
if (src.indexOf("//") === 0) { | ||
cssSrc = `https:${src}`; | ||
} | ||
responses[src] = true; | ||
promises.push(axios.get(cssSrc) | ||
.then((response) => { | ||
responses[src] = response; | ||
})); | ||
externalSrcContent[src] = true; | ||
promises.push( | ||
axios.get(cssSrc).then(response => { | ||
externalSrcContent[src] = response; | ||
}) | ||
); | ||
} | ||
@@ -72,69 +124,10 @@ } catch (err) { | ||
/* html ⚡ */ | ||
$('html').each((index, element) => { | ||
$(element).attr('amp', ''); | ||
}); | ||
/* head */ | ||
/* main amp library */ | ||
$('head script[src="https://cdn.ampproject.org/v0.js"]').remove(); | ||
$('head').prepend('<script async src="https://cdn.ampproject.org/v0.js"></script>'); | ||
/* meta charset */ | ||
$('head meta[charset="utf-8"]').remove(); | ||
$('head meta[charset="UTF-8"]').remove(); | ||
$('head').prepend('<meta charset="utf-8">'); | ||
/* google analytics */ | ||
$('script').each((index, element) => { | ||
const src = $(element).attr('src'); | ||
if (src) { | ||
const trackingId = src.match(/\bUA-\d{4,10}-\d{1,4}\b/); | ||
if (trackingId) { | ||
$(element).remove(); | ||
$('head').prepend('<script async custom-element="amp-analytics"src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>'); | ||
$('body').append(`<amp-analytics type="googleanalytics"> | ||
<script type="application/json"> | ||
{ "vars": { | ||
"account": "${trackingId}" | ||
}, | ||
"triggers": { | ||
"trackPageview": { | ||
"on": "visible", | ||
"request": "pageview" | ||
} | ||
} | ||
} | ||
</script> | ||
</amp-analytics>`); | ||
} | ||
} | ||
const scriptContent = $(element).html(); | ||
const htmlScriptContent = scriptContent.match(/function gtag\(\){dataLayer\.push\(arguments\);}/); | ||
if (scriptContent && htmlScriptContent) { | ||
$(element).remove(); | ||
} | ||
}); | ||
/* meta viewport */ | ||
if ($('head meta[content="width=device-width,minimum-scale=1,initial-scale=1"]').length === 0) { | ||
$('head').append('<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">'); | ||
} | ||
/* style amp-boilerplate */ | ||
if ($('head style[amp-boilerplate]').length === 0) { | ||
$('head').append('<style amp-boilerplate="">body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate="">body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>'); | ||
} | ||
/* body */ | ||
/* img dimensions */ | ||
$('img:not([width]):not([height])').each((index, element) => { | ||
const src = $(element).attr('src'); | ||
/* Set <img/> element height/width dimensions */ | ||
$("img:not([width]):not([height])").each((index, element) => { | ||
const src = $(element).attr("src"); | ||
if (!src) { | ||
return $(element).remove(); | ||
} | ||
if (src.indexOf('//') === -1) { | ||
const image = `${options.cwd}/${$(element).attr('src')}`; | ||
if (src.indexOf("//") === -1) { | ||
const image = `${options.cwd}/${$(element).attr("src")}`; | ||
if (fs.existsSync(image)) { | ||
@@ -144,14 +137,14 @@ const size = sizeOf(image); | ||
width: round(size.width), | ||
height: round(size.height), | ||
height: round(size.height) | ||
}); | ||
} | ||
} else if (src.indexOf('//') !== -1) { | ||
const response = responses[src]; | ||
} else if (src.indexOf("//") !== -1) { | ||
const response = externalSrcContent[src]; | ||
if (response === true) { | ||
throw new Error('No image for', src); | ||
throw new Error("No image for", src); | ||
} | ||
const size = sizeOf(Buffer.from(response.data, 'binary')); | ||
const size = sizeOf(Buffer.from(response.data, "binary")); | ||
$(element).attr({ | ||
width: round(size.width), | ||
height: round(size.height), | ||
height: round(size.height) | ||
}); | ||
@@ -161,24 +154,27 @@ } | ||
/* inline styles */ | ||
$('link[rel=stylesheet]').each((index, element) => { | ||
const src = $(element).attr('href'); | ||
/* Fetch external stylesheet content */ | ||
let styles = ""; | ||
$("link[rel=stylesheet]").each((index, element) => { | ||
const src = $(element).attr("href"); | ||
let path = src; | ||
let file = ''; | ||
const setFile = (data) => { | ||
const minified = new CleanCss().minify(data).styles; | ||
return `<style amp-custom>${minified}</style>`; | ||
}; | ||
try { | ||
if (src.indexOf('//') === -1) { | ||
if (isGoogleFontHostSrc(src)) { | ||
return; | ||
} | ||
const isRemoteSrcReference = src.indexOf("//") !== -1; | ||
if (isRemoteSrcReference) { | ||
const response = externalSrcContent[src]; | ||
if (response === true) { | ||
throw new Error("No CSS for", src); | ||
} | ||
styles += response.data; | ||
} else { | ||
// Local file reference | ||
path = `${options.cwd}/${src}`; | ||
if (fs.existsSync(path)) { | ||
file = setFile(String(fs.readFileSync(path))); | ||
const fileContent = String(fs.readFileSync(path)); | ||
styles += fileContent; | ||
} | ||
} else if (src.indexOf('//') !== -1) { | ||
const response = responses[src]; | ||
if (response === true) { | ||
throw new Error('No CSS for', src); | ||
} | ||
file = setFile(response.data); | ||
} | ||
@@ -188,12 +184,28 @@ } catch (err) { | ||
} | ||
$(element).replaceWith(file); | ||
$(element).remove(); | ||
}); | ||
/* Gather internal styles */ | ||
$("style:not([amp-boilerplate])").each((index, element) => { | ||
styles += $(element).html(); | ||
$(element).remove(); | ||
}); | ||
// Add styles to <head/> | ||
const minifiedStyles = new CleanCss().minify(styles).styles; | ||
const finalizedStyles = scrubCss(minifiedStyles); | ||
headElement.find("style[amp-custom]").remove(); | ||
headElement.append(`<style amp-custom>${finalizedStyles}</style>`); | ||
/* youtube */ | ||
$('iframe[src*="http://www.youtube.com"],iframe[src*="https://www.youtube.com"],iframe[src*="http://youtu.be/"],iframe[src*="https://youtu.be/"]').each((index, element) => { | ||
youtube = true; | ||
const src = $(element).attr('src'); | ||
const width = $(element).attr('width'); | ||
const height = $(element).attr('height'); | ||
const path = url.parse(src).pathname.split('/'); | ||
let youTubeVideoEmbeded = false; | ||
$( | ||
'iframe[src*="http://www.youtube.com"],iframe[src*="https://www.youtube.com"],iframe[src*="http://youtu.be/"],iframe[src*="https://youtu.be/"]' | ||
).each((index, element) => { | ||
youTubeVideoEmbeded = true; | ||
const src = $(element).attr("src"); | ||
const width = $(element).attr("width"); | ||
const height = $(element).attr("height"); | ||
const path = url.parse(src).pathname.split("/"); | ||
const ampYoutube = ` | ||
@@ -209,10 +221,15 @@ <amp-youtube | ||
if (youtube) { | ||
$('head').prepend('<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js">'); | ||
if (youTubeVideoEmbeded) { | ||
headElement.prepend( | ||
'<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js">' | ||
); | ||
} | ||
/* amp tags */ | ||
$(tags.amp.join(',')).each((index, element) => { | ||
/* Convert HTML tags to AMP tags */ | ||
const includeTags = { | ||
amp: ["img", "video"] | ||
}; | ||
$(includeTags.amp.join(",")).each((index, element) => { | ||
const ampElement = Object.assign(element, { | ||
name: `amp-${element.name}`, | ||
name: `amp-${element.name}` | ||
}); | ||
@@ -222,3 +239,13 @@ $(element).html($(ampElement).html()); | ||
return $.html(); | ||
const outerHtml = $.html(); | ||
const prettyOuterHtml = pretty(outerHtml); | ||
return prettyOuterHtml; | ||
}; | ||
function isGoogleFontHostSrc(src) { | ||
return src.indexOf("fonts.googleapis.com") > -1; | ||
} | ||
function scrubCss(css) { | ||
return css.replace(/\!important/g, ""); | ||
} |
{ | ||
"name": "@bradymholt/ampify", | ||
"version": "0.1.0", | ||
"publishConfig":{ | ||
"version": "0.2.0", | ||
"publishConfig": { | ||
"access": "public" | ||
@@ -42,3 +42,4 @@ }, | ||
"clean-css": "^4.2.1", | ||
"image-size": "^0.7.3" | ||
"image-size": "^0.7.3", | ||
"pretty": "^2.0.0" | ||
}, | ||
@@ -45,0 +46,0 @@ "devDependencies": { |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
12775
238
5
1
+ Addedpretty@^2.0.0
+ Added@isaacs/cliui@8.0.2(transitive)
+ Added@one-ini/wasm@0.1.1(transitive)
+ Added@pkgjs/parseargs@0.11.0(transitive)
+ Addedabbrev@2.0.0(transitive)
+ Addedansi-regex@5.0.16.0.1(transitive)
+ Addedansi-styles@4.3.06.2.1(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@2.0.1(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedcommander@10.0.1(transitive)
+ Addedcondense-newlines@0.2.1(transitive)
+ Addedconfig-chain@1.1.13(transitive)
+ Addedcross-spawn@7.0.3(transitive)
+ Addedeastasianwidth@0.2.0(transitive)
+ Addededitorconfig@1.0.4(transitive)
+ Addedemoji-regex@8.0.09.2.2(transitive)
+ Addedextend-shallow@2.0.1(transitive)
+ Addedforeground-child@3.1.1(transitive)
+ Addedglob@10.3.16(transitive)
+ Addedini@1.3.8(transitive)
+ Addedis-buffer@1.1.6(transitive)
+ Addedis-extendable@0.1.1(transitive)
+ Addedis-fullwidth-code-point@3.0.0(transitive)
+ Addedis-whitespace@0.3.0(transitive)
+ Addedisexe@2.0.0(transitive)
+ Addedjackspeak@3.1.2(transitive)
+ Addedjs-beautify@1.15.1(transitive)
+ Addedjs-cookie@3.0.5(transitive)
+ Addedkind-of@3.2.2(transitive)
+ Addedlru-cache@10.2.2(transitive)
+ Addedminimatch@9.0.1(transitive)
+ Addedminipass@7.1.1(transitive)
+ Addednopt@7.2.1(transitive)
+ Addedpath-key@3.1.1(transitive)
+ Addedpath-scurry@1.11.1(transitive)
+ Addedpretty@2.0.0(transitive)
+ Addedproto-list@1.2.4(transitive)
+ Addedsemver@7.6.2(transitive)
+ Addedshebang-command@2.0.0(transitive)
+ Addedshebang-regex@3.0.0(transitive)
+ Addedsignal-exit@4.1.0(transitive)
+ Addedstring-width@4.2.35.1.2(transitive)
+ Addedstrip-ansi@6.0.17.1.0(transitive)
+ Addedwhich@2.0.2(transitive)
+ Addedwrap-ansi@7.0.08.1.0(transitive)