@cloudcannon/suite
Version:
A suite of gulp tools to manage static sites on CloudCannon
324 lines (269 loc) • 8.95 kB
JavaScript
var Vinyl = require("vinyl"),
PluginError = require("plugin-error"),
fs = require("fs"),
c = require("ansi-colors"),
through = require("through2").obj,
sortObject = require("sort-object-keys"),
log = require("fancy-log"),
cheerio = require("cheerio"),
crypto = require("crypto"),
path = require("path");
const REDIRECT_HTML = fs.readFileSync(__dirname + "/redirect-page.html", "utf8");
var IGNORE_URL_REGEX = /^([a-z]+\:|\/\/|\#)/;
function cleanObj(obj) {
for (var prop in obj) {
if (!obj[prop]) {
delete obj[prop];
}
}
}
function getBaseFolder(sitePath) {
return sitePath.replace(/^\/+/, "").split(path.sep).shift();
}
function generateDefaultI18nKey($, $el) {
var outerHTML = $.html($el);
return crypto.createHash("sha256").update(outerHTML).digest("base64").replace(/=+$/, "");
}
function handleHTMLFile(options) {
options = options || {};
return through(function (file, encoding, callback) {
if (file.isNull()) {
return callback(null, file);
}
if (file.isStream()) {
return callback(new PluginError("local-ejs", "Streaming not supported"));
}
file.sitePath = "/" + file.path.substring(file.base.length);
file.sitePath = file.sitePath.replace(/\/index.html?/i, "/").replace(/\/+/i, "/");
if (options.skipFile && options.skipFile(file)) {
if (options.showSkippedUpdates) {
log("Skipping HTML " + c.grey("'" + file.sitePath + "'"));
}
return callback();
}
var $ = cheerio.load(file.contents.toString(encoding), { _useHtmlParser2: true, lowerCaseAttributeNames:false, decodeEntities: false });
$("[data-i18n]").each(function processElement() {
var $el = $(this),
key = $el.attr("data-i18n"),
attributes = $el.attr("data-i18n-attrs");
if (!key) {
key = generateDefaultI18nKey($, $el);
}
attributes = attributes ? attributes.split(",") : [];
attributes.map(function (attr) { return attr.trim(); });
options.processElement.apply(this, [file, $el, key, attributes, $]);
});
if (options.rewriteLinks) {
$("a[href]:not([preserve-link]), link[href]:not([preserve-link])").each(function processLink() {
var $el = $(this),
href = $el.attr("href"),
updated = href && options.rewriteLinks.apply(this, [file, href]);
if (updated) {
$el.attr("href", updated);
}
});
$("meta[http-equiv='refresh']").each(function processLink() {
var $el = $(this),
content = $el.attr("content"),
parts = content.split(";");
for (var i = 0; i < parts.length; i++) {
if (parts[i].indexOf("url=") === 0) {
var href = parts[i].substring(4),
updated = options.rewriteLinks.apply(this, [file, href]);
if (updated) {
parts[i] = "url=" + updated;
$el.attr("content", parts.join(";"));
}
return;
}
}
});
}
if (options.completeFile) {
options.completeFile.apply(this, [file, $]);
}
callback();
}, options.complete);
}
module.exports = {
generate: function (options) {
options = options || {version: 2};
var locale = {};
function addLocale(key, value, file) {
if (!locale[key]) {
locale[key] = {
"original": value,
"pages": {},
"total": 0
};
} else if (locale[key].original !== value && options.showDuplicateLocaleWarnings) {
log(c.yellow("Duplicate & mismatched data-i18n") + " " + c.grey(key));
}
var filePath = file.path.substring(file.base.length);
if (!locale[key].pages[filePath]) {
locale[key].pages[filePath] = 1;
} else {
locale[key].pages[filePath]++;
}
locale[key].total++;
}
return handleHTMLFile({
processElement: function (file, $el, key, attributes) {
addLocale(key, $el.html(), file);
attributes.forEach(function (attr) {
addLocale(key + "." + attr, $el.attr(attr) || "", file);
});
},
complete: function (callback) {
var sorted = sortObject(locale);
cleanObj(sorted);
var keys = Object.keys(sorted);
var contents;
switch (options.version) {
case 2:
contents = {
"version": 2,
"keys": sorted
};
break;
default:
log(c.yellow("Using legacy format, please use 2 for i18n.source_version"));
contents = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
contents[key] = sorted[key].original;
}
}
this.push(new Vinyl({
path: "source.json",
contents: Buffer.from(JSON.stringify(contents, null, options.delimeter || ""))
}));
log(c.green("Generation complete") + " "
+ c.blue("i18n/source.json")
+ " available with " + keys.length + " keys");
callback();
}
});
},
translate: function (options) {
options = options || {};
var targetLocale = options.targetLocale,
locale = options.locales[targetLocale],
localeNames = options.localeNames;
var localeLookup = {};
for (var i = 0; i < localeNames.length; i++) {
localeLookup[localeNames[i]] = true;
}
return handleHTMLFile({
skipFile: function (file) {
var baseFolder = getBaseFolder(file.sitePath);
return !!localeLookup[baseFolder];
},
rewriteLinks: function rewriteLinks(file, href) {
if (!href || IGNORE_URL_REGEX.test(href)) {
return;
}
var baseFolder = getBaseFolder(href);
if (localeLookup[baseFolder]) {
if (options.showSkippedUpdates) {
log("Skipping link " + c.grey("'" + href + "'"));
}
return;
}
var parsed = path.parse(href);
if (parsed.ext && parsed.ext.indexOf(".htm") !== 0) {
return;
}
var parts = href.replace(/^\/+/, "").split("/");
parts.unshift(targetLocale);
var updated = "/" + parts.join("/");
return updated.replace(/\/+/g, "/");
},
processElement: function (file, $el, key, attributes) {
if (!locale) {
return; // Default locale case
}
if (locale[key]) {
$el.html(locale[key].wrappedTranslation || locale[key].translation);
locale[key].count++;
} else if ($el.html() && options.showMissingLocaleWarnings) {
log(c.yellow("Missing translation") + " "
+ c.grey(targetLocale + file.sitePath) +
" [data-i18n=" + key + "]");
}
attributes.forEach(function (attr) {
if (locale[key + "." + attr]) {
$el.attr(attr, locale[key + "." + attr].translation);
locale[key + "." + attr].count++;
} else if ($el.attr(attr) && options.showMissingLocaleWarnings) {
log(c.yellow("Missing translation") + " "
+ c.grey(targetLocale + file.sitePath) +
" [data-i18n=" + key + "][" + attr + "]");
}
});
},
completeFile: function (file, $) {
$("html").attr("lang", targetLocale);
$("meta[http-equiv='content-language']").remove();
$("head").append('<meta http-equiv="content-language" content="' + targetLocale + '">\n');
if (options.addOtherLocaleAlternates) {
localeNames.forEach(function (localeName) {
if (localeName != targetLocale) {
var redirectUrl = localeName + file.sitePath;
$("head").append('<link rel="alternate" href="/' + redirectUrl + '" hreflang="' + localeName + '">\n');
}
});
}
file.contents = Buffer.from($.html());
this.push(file);
}
});
},
redirectPage: function (options) {
options = options || {};
var defaultLocale = options.defaultLocale,
localeNames = options.localeNames;
var localeLookup = {};
for (var i = 0; i < localeNames.length; i++) {
localeLookup[localeNames[i]] = true;
}
var redirectLookup = {};
localeNames.forEach(function (localeName) {
var match = localeName.toLowerCase().match(/[a-z]+/gi);
var language = match[0], country = match[1];
redirectLookup[language] = localeName;
if (country) {
redirectLookup[language + "-" + country] = localeName;
redirectLookup[language + "_" + country] = localeName;
}
redirectLookup[localeName] = localeName;
});
return through(function (file, encoding, callback) {
if (file.isNull()) {
return callback(null, file);
}
if (file.isStream()) {
return callback(new PluginError("local-ejs", "Streaming not supported"));
}
file.sitePath = "/" + file.path.substring(file.base.length);
file.sitePath = file.sitePath.replace(/\/index.html?/i, "/").replace(/\/+/i, "/");
var baseFolder = getBaseFolder(file.sitePath);
if (file.sitePath === "/404.html" || localeLookup[baseFolder]) {
return callback();
}
var html = REDIRECT_HTML
.replace(/ALTERNATES/g, localeNames.map(function (targetLocale) {
if (targetLocale !== defaultLocale) {
return '<link rel="alternate" href="/' + targetLocale + file.sitePath + '" hreflang="' + targetLocale + '">';
}
return '';
}).join(""))
.replace(/SITE_PATH/g, file.sitePath)
.replace(/DEFAULT_LANGUAGE/g, defaultLocale)
.replace(/LOCALE_LOOKUP/g, JSON.stringify(redirectLookup));
file.contents = Buffer.from(html);
this.push(file);
callback();
});
}
};