google-fonts-offline
Version:
Use Google Fonts while you're offline or in development.
240 lines (179 loc) • 7.37 kB
JavaScript
var fs = require('fs'),
path = require('path'),
stream = require('stream'),
url = require('url'),
http = require('http'),
// Group[0]
// vvvvvvvv
fontFaceRulePattern = /(?:@font-face\s*\{\s*)([^\{\}]+)/gi,
//
// Group[1] Group[2]
// vvvvvv vvvvvv
fontUrlPattern = /url\([\'\"]?([^\)]+?)[\'\"]?\)(?:\s*format\([\'\"]?([^\)]+?)[\'\"]?\))?/gi,
//
// Group[1]
// Group[0] vvvvvvv
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
fontFamilyPattern = /font-family\s*\:\s*[\'"]?([^\;]+?)[\'"]?\;/i,
fontWeightPattern = /font-weight\s*\:\s*[\'"]?([^\;]+?)[\'"]?\;/i,
fontStylePattern = /font-style\s*\:\s*[\'"]?([^\;]+?)[\'"]?\;/i,
scriptOutputDir = 'fonts',
cssFilename = 'fonts.css',
fontFaceBulletproofStyle = fs.readFileSync(path.join(path.dirname(fs.realpathSync(__filename)), 'fontface.css.tpl')).toString();
fileExts = {
"embedded-opentype" : ".eot",
"woff" : ".woff",
"woff2" : ".woff2",
"truetype" : ".ttf",
"svg" : ".svg"
},
userAgents = [
// EOT lives in IE
"Mozilla/5.0 (MSIE 8.0; Windows NT 6.1; Trident/4.0)",
// WOFF for Windows
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
// TTF needs Opera
"Opera/9.80 (Macintosh; Intel Mac OS X; U; en) Presto/2.2.15 Version/10.00",
// SVG loves iPad
"Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10",
// WOFF for *nix ("Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19")
// http://michaelboeke.com/blog/2013/09/10/Self-hosting-Google-web-fonts/
// WOFF2 for Safari
"Mozilla/5.0 (Windows NT 6.1; rv:43.0) Gecko/20100101 Firefox/43.0",
],
fontfaceList = {},
requestsInProgress = 0,
urlToProcess = '',
httpOptions = {
'hostname' : '',
'path' : '',
'headers' : {}
};
exports.setOutputDir = function(dirName) {
scriptOutputDir = dirName || scriptOutputDir;
};
exports.setCssFilename = function(filename) {
cssFilename = filename || cssFilename;
};
exports.download = function(urlToProcess) {
if (!urlToProcess) {
console.log('No URL to process! Use something like this: http://fonts.googleapis.com/css?family=Open+Sans|Roboto');
process.exit(0);
}
httpOptions.hostname = url.parse(urlToProcess).hostname;
httpOptions.path = url.parse(urlToProcess).path;
createOutputDir(scriptOutputDir);
processGoogleFontsUrl(userAgents.shift());
(function awaitHttpRequests() {
setTimeout(function() {
requestsInProgress > 0 ? awaitHttpRequests() : buildOutputCssFile();
}, 50);
}());
};
function createOutputDir(dirName) {
var dirPath = path.join(process.cwd(), dirName);
fs.exists(dirPath, function(exists) {
if(!exists) fs.mkdirSync(dirPath);
});
}
function processGoogleFontsUrl(userAgent) {
if (!userAgent) return;
console.log("Mimics to %s...", userAgent);
requestsInProgress += 1;
httpCallback = function(response) {
var buffer = [];
response.on('data', function(chunk) {
buffer.push(chunk);
});
response.on('end', function() {
var css = Buffer.concat(buffer).toString();
css.match(fontFaceRulePattern).forEach(processFontface);
requestsInProgress -= 1;
processGoogleFontsUrl(userAgents.shift());
});
};
httpOptions.headers['User-Agent'] = userAgent;
http.get(httpOptions, httpCallback)
.on('error', handleHttpError);
}
function processFontface(css) {
var urlMatches,
fontKey = composeFontFaceKey(css);
while ((urlMatches = fontUrlPattern.exec(css)) !== null) {
// eot have no "src(...) format(...)" form, only "src(...);"
var format = urlMatches[2] || "embedded-opentype";
fontUrlWithFormat = format + '|' + urlMatches[1],
filename = fontKey.replace(/\s/g, '+').replace(/:/g, '_'),
extension = fileExts[format];
// There is no font in collection with such a fontKey
if (!fontfaceList[fontKey]) {
downloadFont(urlMatches[1], filename + extension);
fontfaceList[fontKey] = [fontUrlWithFormat];
continue;
}
// There is no font url in collection already
if (fontfaceList[fontKey].indexOf(fontUrlWithFormat) === -1) {
downloadFont(urlMatches[1], filename + extension);
fontfaceList[fontKey].push(fontUrlWithFormat);
}
}
}
function composeFontFaceKey(css) {
var family = css.match(fontFamilyPattern)[1],
weight = css.match(fontWeightPattern)[1],
style = css.match(fontStylePattern)[1];
return family + ':' + weight + ':' + style;
}
function downloadFont(url, filename) {
console.log(" Downloading %s...", filename);
requestsInProgress += 1;
http.get(url, function(response) {
var buffer = [];
response.on('data', function(chunk) {
buffer.push(chunk);
});
response.on('end', function() {
fs.writeFileSync(path.join(scriptOutputDir, filename), Buffer.concat(buffer));
requestsInProgress -= 1;
});
})
.on('error', handleHttpError);
}
function buildOutputCssFile() {
var i,
fontKey,
splittedArray,
fontFamily,
fontWeight,
fontStyle,
fontFormat,
fontGoogleFontsUrl,
outputCss = '';
console.log('Composing CSS file...');
for (fontKey in fontfaceList) {
splittedArray = fontKey.split(':');
fontFamily = splittedArray[0];
fontWeight = splittedArray[1];
fontStyle = splittedArray[2];
fontFilename = fontKey.replace(/\s/g, '+').replace(/:/g, '_');
outputCss = fontFaceBulletproofStyle
.replace(/\{\{fontFilename\}\}/g, fontFilename)
.replace(/\{\{fontFamily\}\}/g, fontFamily)
.replace(/\{\{fontWeight\}\}/g, fontWeight)
.replace(/\{\{fontStyle\}\}/g, fontStyle);
for (i = fontfaceList[fontKey].length - 1; i >= 0; i--) {
splittedArray = fontfaceList[fontKey][i].split('|');
fontFormat = splittedArray[0];
fontGoogleFontsUrl = splittedArray[1];
outputCss = outputCss
.replace(new RegExp('\\{\\{' + fontFormat + '-gf-url\\}\\}', 'g'), fontGoogleFontsUrl);
}
// Should be sync! Fontface declarations order matters!
fs.appendFileSync(path.join(scriptOutputDir, cssFilename), outputCss);
}
console.log('Done!');
}
function handleHttpError(error) {
requestsInProgress -= 1;
console.log('Request error: %s', error.message);
}