UNPKG

assetgraph-builder-esprima

Version:

Build system for web sites and applications

231 lines (210 loc) 11.8 kB
var _ = require('lodash'), estraverse = require('estraverse'), esanimate = require('esanimate'), i18nTools = require('../i18nTools'), htmlLangCssSelectorRegExp = /(?:^|,\s*)(html\[\s*lang\s*(?:=|\|=|~=)\s*(|'|").*?\1\s*\])(.*)$/i; function isLeftHandSideOfAssignment(stack, topNode) { function getItem(i) { if (i === stack.length) { return topNode; } else { return stack[i]; } } for (var i = stack.length ; i >= 0 ; i -= 1) { var node = getItem(i); if (node.type === 'AssignmentExpression') { if (getItem(i + 1) === node.left) { return true; } else { break; } } else if (node.type === 'VariableDeclarator') { if (getItem(i + 1) === node.id.name) { return true; } else { break; } } } return false; } function assetNeedsLocalization(asset, assetGraph) { var needsLocalization = false; if (asset.type === 'JavaScript') { if (asset.incomingRelations.every(function (incomingRelation) { return incomingRelation.type !== 'HtmlScript' || incomingRelation.node.id !== 'bootstrapper'; })) { estraverse.traverse(asset.parseTree, { enter: function (node) { // TODO: The presence of only LOCALECOOKIENAME/SUPPORTEDLOCALEIDS/DEFAULTLOCALEID don't really require the asset to be cloned as // they just need to be replaced to the same value in each locale. if ((node.type === 'Identifier' && /^(?:LOCALEID|SUPPORTEDLOCALEIDS|DEFAULTLOCALEID|LOCALECOOKIENAME)$/.test(node.name) && !isLeftHandSideOfAssignment(this.parents(), node)) || (node.type === 'CallExpression' && node.callee.type === 'Identifier' && /^TR(?:PAT)?$/.test(node.callee.name))) { needsLocalization = true; return this.break(); } } }); } } else if (asset.isHtml || asset.isSvg) { i18nTools.eachI18nTagInHtmlDocument(asset.parseTree, function (options) { if (options.key !== null) { needsLocalization = true; return false; } }); } else if (asset.type === 'Css') { asset.eachRuleInParseTree(function (cssRule, parentRuleOrStylesheet) { if (cssRule.type === 1) { // cssom.CSSRule.STYLE_RULE if (htmlLangCssSelectorRegExp.test(cssRule.selectorText)) { needsLocalization = true; return false; } } }); } return needsLocalization; } function isBootstrapperRelation(relation) { return relation.type === 'HtmlScript' && relation.node && relation.node.getAttribute('id') === 'bootstrapper'; } function followRelationFn(relation) { return relation.type !== 'HtmlAnchor' && relation.type !== 'HtmlMetaRefresh' && relation.type !== 'SvgAnchor' && relation.type !== 'JavaScriptSourceUrl' && !isBootstrapperRelation(relation); } module.exports = function (queryObj, options) { var localeIds = options.localeIds.map(i18nTools.normalizeLocaleId), defaultLocaleId = i18nTools.normalizeLocaleId(options.defaultLocaleId || 'en'); return function cloneForEachLocale(assetGraph) { assetGraph.findAssets(_.extend({type: 'Html'}, queryObj)).forEach(function (originalHtmlAsset) { var assetToLocalizeById = {}, assetsToLocalize = []; assetGraph.eachAssetPostOrder(originalHtmlAsset, followRelationFn, function (asset) { if (asset.isLoaded && (assetNeedsLocalization(asset) || assetGraph.findRelations({from: asset}).some(function (outgoingRelation) {return outgoingRelation.to.id in assetToLocalizeById;}))) { assetToLocalizeById[asset.id] = asset; assetsToLocalize.push(asset); } }); var nonInlineAssetsToLocalizePreOrder = []; assetGraph.eachAssetPreOrder(originalHtmlAsset, followRelationFn, function (asset) { if (!asset.isInline && asset.id in assetToLocalizeById) { nonInlineAssetsToLocalizePreOrder.push(asset); } }); localeIds.forEach(function (localeId) { var allKeysForLocale = i18nTools.extractAllKeysForLocale(assetGraph, localeId), trReplacer = i18nTools.createTrReplacer({ allKeysForLocale: allKeysForLocale, localeId: localeId, defaultLocaleId: defaultLocaleId }), i18nTagReplacer = i18nTools.createI18nTagReplacer({ allKeysForLocale: allKeysForLocale, localeId: localeId, defaultLocaleId: defaultLocaleId }), globalValueByName = {LOCALEID: localeId, SUPPORTEDLOCALEIDS: localeIds, DEFAULTLOCALEID: defaultLocaleId}; ['localeCookieName'].forEach(function (optionName) { if (options[optionName]) { globalValueByName[optionName.toUpperCase()] = options[optionName]; } }); var localizedAssets = []; function localizeAsset(asset) { if (asset.type === 'JavaScript') { if (asset.incomingRelations.every(isBootstrapperRelation)) { return; } i18nTools.eachTrInAst(asset.parseTree, trReplacer); estraverse.replace(asset.parseTree, { enter: function (node) { if (node.type === 'Identifier' && globalValueByName.hasOwnProperty(node.name) && !isLeftHandSideOfAssignment(this.parents(), node)) { return esanimate.astify(globalValueByName[node.name]); } } }); asset.markDirty(); } else if (asset.isHtml || asset.isSvg) { var document = asset.parseTree; i18nTools.eachI18nTagInHtmlDocument(document, i18nTagReplacer); if (document.documentElement) { document.documentElement.setAttribute('lang', localeId); } asset.markDirty(); } else if (asset.type === 'Css') { var cssRules = []; asset.eachRuleInParseTree(function (cssRule) { if (cssRule.type === 1) { // cssom.CSSRule.STYLE_RULE cssRules.push(cssRule); } }); // Traverse the rules in reverse so the indices aren't screwed up by deleting rules underway: cssRules.reverse().forEach(function (cssRule) { var matchSelectorText = cssRule.selectorText.match(htmlLangCssSelectorRegExp); if (matchSelectorText) { var rewrittenSelectorFragments = []; cssRule.selectorText.split(/\s*,\s*/).forEach(function (selectorFragment) { var matchSelectorFragment = selectorFragment.match(/(html\[\s*lang\s*(?:=|\|=|~=)\s*(|'|").*?\1\s*\])(.*)$/i); if (matchSelectorFragment) { if (localizedAssets[0].parseTree.querySelectorAll(matchSelectorFragment[1]).length > 0) { rewrittenSelectorFragments.push('html' + matchSelectorFragment[3]); } } else { rewrittenSelectorFragments.push(selectorFragment); } }); if (rewrittenSelectorFragments.length > 0) { cssRule.selectorText = rewrittenSelectorFragments.join(', '); } else { assetGraph.findRelations({from: asset}).forEach(function (outgoingRelation) { if (outgoingRelation.cssRule !== cssRule) { return; } if (outgoingRelation.to.isInline) { assetGraph.removeAsset(outgoingRelation.to); } // FIXME: Find out why outgoingRelation isn't in the graph when outgoingRelation.to is an inline image! if (outgoingRelation.assetGraph) { assetGraph.removeRelation(outgoingRelation); } }); var containingCssRules = (cssRule.parentRule || cssRule.parentStyleSheet).cssRules; containingCssRules.splice(containingCssRules.indexOf(cssRule), 1); } } }); asset.markDirty(); } } // asset.clone adds the asset to the graph and populates the relations. Since the localization // could introduce or remove relations, it needs to happen before the population. // The 'addAsset' event is emitted right before the population takes place, so that does the job. // (Would of course be cleaner if asset.clone was split up or supported a hook at this point). assetGraph.on('addAsset', localizeAsset); nonInlineAssetsToLocalizePreOrder.forEach(function (asset) { var incomingRelationsFromLocalizedAssets = assetGraph.findRelations({to: asset, from: {nonInlineAncestor: localizedAssets}}), targetUrl = asset.url.replace(/(\.\w+)?$/, '.' + localeId + '$1'), existingLocalizedAsset = assetGraph.findAssets({url: targetUrl})[0]; if (existingLocalizedAsset) { incomingRelationsFromLocalizedAssets.forEach(function (incomingRelation) { incomingRelation.to = existingLocalizedAsset; incomingRelation.refreshHref(); }); } else { var localizedAsset = asset.clone(incomingRelationsFromLocalizedAssets); localizedAsset.url = targetUrl; localizedAssets.push(localizedAsset); } }); assetGraph.removeListener('addAsset', localizeAsset); }); // Remove orphaned JavaScript and templates: nonInlineAssetsToLocalizePreOrder.forEach(function (asset) { if (asset.assetGraph && (asset === originalHtmlAsset || (!asset.isInline && assetGraph.findRelations({to: asset}).length === 0))) { assetGraph.removeAsset(asset); } }); }); }; };