Skip to content

Commit

Permalink
[BREAKING] Only rewrite image paths for RTL-variant when files exist (#…
Browse files Browse the repository at this point in the history
…162)

Only rewrite img path to img-RTL in RTL (right-to-left) CSS if image file is present in img-RTL folder.
Don't rewrite img paths with a protocol (http/https/data/...) or starting with a slash (`/`).

BREAKING CHANGE:

This affects the output of the `rtl` (right-to-left) variant that is created by applying several mechanisms to create a mirrored variant of the CSS to be used in locales that use a right to left text direction.

One aspect is adopting urls which contain an `img` folder in the path.
Before this change, all urls have been changed to use a `img-RTL` folder instead. This allows mirrored images to be used, but it also requires an images to be available on that path even when the original image should be used (e.g. for a logo).

With this change:
- Urls are only adopted in case an `img-RTL` variant of that file exists
- Urls with a protocol (http/https/data/...) or starting with a slash (`/`) are not adopted anymore

Co-authored-by: Matthias Osswald <[email protected]>
  • Loading branch information
tobiasso85 and matz3 authored Mar 10, 2021
1 parent 41b9ba6 commit 88e7a74
Show file tree
Hide file tree
Showing 16 changed files with 519 additions and 49 deletions.
22 changes: 20 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const fileUtilsFactory = require("./fileUtils");
// Plugins
const ImportCollectorPlugin = require("./plugin/import-collector");
const VariableCollectorPlugin = require("./plugin/variable-collector");
const UrlCollector = require("./plugin/url-collector");

// Workaround for a performance issue in the "css" parser module when used in combination
// with the "colors" module that enhances the String prototype.
Expand Down Expand Up @@ -212,7 +213,7 @@ Builder.prototype.build = function(options) {
resolve(tree);
}
});
}).then(function(tree) {
}).then(async function(tree) {
const result = {};

result.tree = tree;
Expand All @@ -222,10 +223,11 @@ Builder.prototype.build = function(options) {
importMappings: mFileMappings[filename]
});
const oVariableCollector = new VariableCollectorPlugin(options.compiler);
const oUrlCollector = new UrlCollector();

// render to css
result.css = tree.toCSS(Object.assign({}, options.compiler, {
plugins: [oImportCollector, oVariableCollector]
plugins: [oImportCollector, oVariableCollector, oUrlCollector]
}));

// retrieve imported files
Expand All @@ -242,6 +244,22 @@ Builder.prototype.build = function(options) {
if (options.rtl) {
const RTLPlugin = require("./plugin/rtl");
oRTL = new RTLPlugin();

const urls = oUrlCollector.getUrls();

const existingImgRtlUrls = (await Promise.all(
urls.map(async ({currentDirectory, relativeUrl}) => {
const relativeImgRtlUrl = RTLPlugin.getRtlImgUrl(relativeUrl);
if (relativeImgRtlUrl) {
const resolvedImgRtlUrl = path.posix.join(currentDirectory, relativeImgRtlUrl);
if (await that.fileUtils.findFile(resolvedImgRtlUrl, options.rootPaths)) {
return resolvedImgRtlUrl;
}
}
})
)).filter(Boolean);

oRTL.setExistingImgRtlPaths(existingImgRtlUrls);
}

if (oRTL) {
Expand Down
55 changes: 44 additions & 11 deletions lib/plugin/rtl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use strict";

const less = require("../thirdparty/less");
const path = require("path");
const url = require("url");

const cssSizePattern = /(-?[.0-9]+)([a-z]*)/;
const percentagePattern = /^\s*(-?[.0-9]+)%\s*$/;
Expand Down Expand Up @@ -202,11 +204,13 @@ const converterFunctions = {
ruleNode.value.value.forEach(swapLeftRight);
},
url: function(ruleNode) {
ruleNode.value.value.forEach(function(valueObject) {
ruleNode.value.value.forEach((valueObject) => {
if (valueObject.type === "Url") {
replaceUrl(valueObject);
this.replaceUrl(valueObject);
} else if (valueObject.type === "Expression") {
valueObject.value.forEach(replaceUrl);
valueObject.value.forEach((childValueObject) => {
this.replaceUrl(childValueObject);
});
}
});
},
Expand Down Expand Up @@ -566,14 +570,6 @@ function processCursorNode(node) {
}
}

function replaceUrl(node) {
if (node.type === "Url") {
modifyOnce(node, "replaceUrl", function(urlNode) {
urlNode.value.value = urlNode.value.value.replace(urlPattern, urlReplacement);
});
}
}

function endsWith(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
Expand All @@ -588,10 +584,15 @@ function modifyOnce(node, type, fn) {
}
}

/**
*
* @constructor
*/
const LessRtlPlugin = module.exports = function() {
/* eslint-disable new-cap */
this.oVisitor = new less.tree.visitor(this);
/* eslint-enable new-cap */
this.resolvedImgRtlPaths = [];
};

LessRtlPlugin.prototype = {
Expand All @@ -611,5 +612,37 @@ LessRtlPlugin.prototype = {
}

return ruleNode;
},
replaceUrl: function(node) {
if (node.type !== "Url") {
return;
}
modifyOnce(node, "replaceUrl", (urlNode) => {
const imgPath = urlNode.value.value;
const parsedUrl = url.parse(imgPath);
if (parsedUrl.protocol || imgPath.startsWith("/")) {
// Ignore absolute urls
return;
}
const imgPathRTL = LessRtlPlugin.getRtlImgUrl(imgPath);
if (!imgPathRTL) {
return;
}
const resolvedUrl = path.posix.join(urlNode.currentFileInfo.currentDirectory, imgPathRTL);
if (this.existingImgRtlPaths.includes(resolvedUrl)) {
urlNode.value.value = imgPathRTL;
}
});
},
setExistingImgRtlPaths: function(existingImgRtlPaths) {
this.existingImgRtlPaths = existingImgRtlPaths;
}
};

LessRtlPlugin.getRtlImgUrl = function(url) {
if (urlPattern.test(url)) {
return url.replace(urlPattern, urlReplacement);
} else {
return null;
}
};
67 changes: 67 additions & 0 deletions lib/plugin/url-collector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use strict";

const path = require("path");
const url = require("url");
const less = require("../thirdparty/less");

const urlNodeNames = {
"background": true,
"background-image": true,
"content": true,
"cursor": true,
"icon": true,
"list-style-image": true
};

/**
* @constructor
*/
const UrlCollectorPlugin = module.exports = function() {
/* eslint-disable-next-line new-cap */
this.oVisitor = new less.tree.visitor(this);
this.urls = new Map();
};

UrlCollectorPlugin.prototype = {
isReplacing: false,
isPreEvalVisitor: false,
run: function(root) {
return this.oVisitor.visit(root);
},
visitRule: function(ruleNode) {
if (urlNodeNames[ruleNode.name]) {
this.visitUrl(ruleNode);
}
},
visitUrl: function(ruleNode) {
for (const valueObject of ruleNode.value.value) {
if (valueObject.type === "Url") {
this.addUrlFromNode(valueObject);
} else if (valueObject.type === "Expression") {
for (const node of valueObject.value) {
this.addUrlFromNode(node);
}
}
}
},
addUrlFromNode: function(node) {
if (node.type === "Url") {
const relativeUrl = node.value.value;

const parsedUrl = url.parse(relativeUrl);
// Ignore urls with protocol (also includes data urls)
// Ignore server absolute urls
if (parsedUrl.protocol || relativeUrl.startsWith("/")) {
return;
}

const {currentDirectory} = node.currentFileInfo;

const resolvedUrl = path.posix.join(currentDirectory, relativeUrl);
this.urls.set(resolvedUrl, {currentDirectory, relativeUrl});
}
},
getUrls: function() {
return Array.from(this.urls.values());
}
};
118 changes: 118 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"eslint-config-google": "^0.14.0",
"graceful-fs": "^4.2.6",
"mocha": "^8.3.1",
"nyc": "^15.1.0"
"nyc": "^15.1.0",
"sinon": "^9.2.4"
}
}
Loading

0 comments on commit 88e7a74

Please sign in to comment.