From 88e7a7436fc7f197186c780a856bb9c6dff7c582 Mon Sep 17 00:00:00 2001 From: Tobias Sorn Date: Wed, 10 Mar 2021 08:45:58 +0100 Subject: [PATCH] [BREAKING] Only rewrite image paths for RTL-variant when files exist (#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 --- lib/index.js | 22 ++- lib/plugin/rtl.js | 55 +++++-- lib/plugin/url-collector.js | 67 +++++++++ package-lock.json | 118 +++++++++++++++ package.json | 3 +- test/expected/rtl/background.css | 28 ++-- test/expected/rtl/image-url.css | 24 ++-- test/fixtures/rtl/background.less | 6 +- test/fixtures/rtl/img-RTL/column_header.gif | 0 test/fixtures/rtl/img-RTL/column_header2.gif | 0 test/fixtures/rtl/img-RTL/column_header3.gif | 0 test/fixtures/rtl/img-RTL/drop-down_ico.png | 0 test/fixtures/rtl/img-RTL/fg/hi.png | 0 .../rtl/img-RTL/hover_column_header.gif | 0 test/test-url-collector-plugin.js | 135 ++++++++++++++++++ test/test.js | 110 +++++++++++++- 16 files changed, 519 insertions(+), 49 deletions(-) create mode 100644 lib/plugin/url-collector.js create mode 100644 test/fixtures/rtl/img-RTL/column_header.gif create mode 100644 test/fixtures/rtl/img-RTL/column_header2.gif create mode 100644 test/fixtures/rtl/img-RTL/column_header3.gif create mode 100644 test/fixtures/rtl/img-RTL/drop-down_ico.png create mode 100644 test/fixtures/rtl/img-RTL/fg/hi.png create mode 100644 test/fixtures/rtl/img-RTL/hover_column_header.gif create mode 100644 test/test-url-collector-plugin.js diff --git a/lib/index.js b/lib/index.js index c6004580..c0020d09 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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. @@ -212,7 +213,7 @@ Builder.prototype.build = function(options) { resolve(tree); } }); - }).then(function(tree) { + }).then(async function(tree) { const result = {}; result.tree = tree; @@ -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 @@ -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) { diff --git a/lib/plugin/rtl.js b/lib/plugin/rtl.js index 6bb1eb6a..3738b207 100644 --- a/lib/plugin/rtl.js +++ b/lib/plugin/rtl.js @@ -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*$/; @@ -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); + }); } }); }, @@ -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; } @@ -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 = { @@ -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; } }; diff --git a/lib/plugin/url-collector.js b/lib/plugin/url-collector.js new file mode 100644 index 00000000..987c1890 --- /dev/null +++ b/lib/plugin/url-collector.js @@ -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()); + } +}; diff --git a/package-lock.json b/package-lock.json index 09eb19db..a34a4e62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -373,6 +373,41 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, + "@sinonjs/commons": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", + "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -1571,6 +1606,12 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1725,6 +1766,12 @@ "minimist": "^1.2.5" } }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1790,6 +1837,12 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "log-symbols": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", @@ -2037,6 +2090,19 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nise": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -2344,6 +2410,15 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2716,6 +2791,43 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "sinon": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", + "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/samsam": "^5.3.1", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -2913,6 +3025,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", diff --git a/package.json b/package.json index acdb6c18..f8308452 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/test/expected/rtl/background.css b/test/expected/rtl/background.css index 93f4b909..72fee524 100644 --- a/test/expected/rtl/background.css +++ b/test/expected/rtl/background.css @@ -15,7 +15,7 @@ background: red; background: url("chess.png") 60% / 10em #808080 round fixed border-box /* some comment */; background: url("img-RTL/column_header.gif") repeat scroll 0 0 #d2dae8; - background: url("img-RTL/column_header.gif") repeat scroll 90% 0 #d2dae8; + background: url("../rtl/img-RTL/column_header.gif") repeat scroll 90% 0 #d2dae8; background: url("img-RTL/column_header.gif") repeat scroll 0 10% #d2dae8; background: url("chess.png") right top / 10% #808080 round fixed border-box; background: url("chess.png") right 10% / 10% #808080 round fixed border-box; @@ -27,7 +27,7 @@ background: red; background: url("chess.png") 60% / 10em #808080 round fixed border-box /* some comment */; background: url("img-RTL/column_header.gif") repeat scroll 0 0 #d2dae8; - background: url("img-RTL/column_header.gif") repeat scroll 90% 0 #d2dae8; + background: url("../rtl/img-RTL/column_header.gif") repeat scroll 90% 0 #d2dae8; background: url("img-RTL/column_header.gif") repeat scroll 0 10% #d2dae8; background: url("chess.png") right top / 10% #808080 round fixed border-box; background: url("chess.png") right 10% / 10% #808080 round fixed border-box; @@ -40,32 +40,32 @@ background-image: url(img-RTL/column_header.gif); background-image: url('img-RTL/column_header.gif'); background-image: url("img-RTL/column_header.gif") /* some comment */; - background-image: url(http://www.x.de/img-RTL/column_header.gif); - background-image: /* some comment */ url('http://www.x.de/img-RTL/column_header.gif'); - background-image: url("http://www.x.de/img-RTL/column_header.gif"); + background-image: url(http://www.x.de/img/column_header.gif); + background-image: /* some comment */ url('http://www.x.de/img/column_header.gif'); + background-image: url("http://www.x.de/img/column_header.gif"); background-image: url(img-RTL/column_header.gif), /* some comment */ url(img-RTL/column_header2.gif); - background-image: url("http://www.x.de/img-RTL/column_header.gif"), url(img-RTL/column_header2.gif); + background-image: url("http://www.x.de/img/column_header.gif"), url(img-RTL/column_header2.gif); background-image: url(img-RTL/column_header.gif), url(img-RTL/column_header2.gif), url(img-RTL/column_header3.gif); } .rtlBackgroundImageMirroringWithMixIn1 { background-image: url(img-RTL/column_header.gif); background-image: url('img-RTL/column_header.gif'); background-image: url("img-RTL/column_header.gif") /* some comment */; - background-image: url(http://www.x.de/img-RTL/column_header.gif); - background-image: /* some comment */ url('http://www.x.de/img-RTL/column_header.gif'); - background-image: url("http://www.x.de/img-RTL/column_header.gif"); + background-image: url(http://www.x.de/img/column_header.gif); + background-image: /* some comment */ url('http://www.x.de/img/column_header.gif'); + background-image: url("http://www.x.de/img/column_header.gif"); background-image: url(img-RTL/column_header.gif), /* some comment */ url(img-RTL/column_header2.gif); - background-image: url("http://www.x.de/img-RTL/column_header.gif"), url(img-RTL/column_header2.gif); + background-image: url("http://www.x.de/img/column_header.gif"), url(img-RTL/column_header2.gif); background-image: url(img-RTL/column_header.gif), url(img-RTL/column_header2.gif), url(img-RTL/column_header3.gif); } .rtlBackgroundImageMirroringWithMixIn2 { background-image: url(img-RTL/column_header.gif); background-image: url('img-RTL/column_header.gif'); background-image: url("img-RTL/column_header.gif") /* some comment */; - background-image: url(http://www.x.de/img-RTL/column_header.gif); - background-image: /* some comment */ url('http://www.x.de/img-RTL/column_header.gif'); - background-image: url("http://www.x.de/img-RTL/column_header.gif"); + background-image: url(http://www.x.de/img/column_header.gif); + background-image: /* some comment */ url('http://www.x.de/img/column_header.gif'); + background-image: url("http://www.x.de/img/column_header.gif"); background-image: url(img-RTL/column_header.gif), /* some comment */ url(img-RTL/column_header2.gif); - background-image: url("http://www.x.de/img-RTL/column_header.gif"), url(img-RTL/column_header2.gif); + background-image: url("http://www.x.de/img/column_header.gif"), url(img-RTL/column_header2.gif); background-image: url(img-RTL/column_header.gif), url(img-RTL/column_header2.gif), url(img-RTL/column_header3.gif); } diff --git a/test/expected/rtl/image-url.css b/test/expected/rtl/image-url.css index 0e3ae1c0..8112bd95 100644 --- a/test/expected/rtl/image-url.css +++ b/test/expected/rtl/image-url.css @@ -5,16 +5,16 @@ list-style-image: url(img-RTL/fg/hi.png); } .rtlImageUrlMirroringAbsolute { - background: url(http://abc.de/img-RTL/fg/hi.png) /* some comment */; + background: url(http://abc.de/img/fg/hi.png) /* some comment */; } .rtlImageUrlMirroringQuotes { - background: url('http://abc.de/img-RTL/fg/hi.png'); - background: /* some comment */ url("http://abc.de/img-RTL/fg/hi.png"); + background: url('http://abc.de/img/fg/hi.png'); + background: /* some comment */ url("http://abc.de/img/fg/hi.png"); background: url('img-RTL/column_header.gif') repeat /* some comment */ scroll 0 0 #d2dae8; background: url("img-RTL/column_header.gif") repeat scroll 0 0 #d2dae8 /* some comment */; } .rtlImageUrlMirroringMultiple { - background: url(http://abc.de/img-RTL/fg/hi.png), /* some comment */ url(http://abc.de/img-RTL/fg/hi.png); + background: url(http://abc.de/img/fg/hi.png), /* some comment */ url(http://abc.de/img/fg/hi.png); } .rtlImageUrlMirroringRelative1 { background: url(img-RTL/fg/hi.png) /* some comment */; @@ -22,16 +22,16 @@ list-style-image: url(img-RTL/fg/hi.png); } .rtlImageUrlMirroringAbsolute1 { - background: url(http://abc.de/img-RTL/fg/hi.png) /* some comment */; + background: url(http://abc.de/img/fg/hi.png) /* some comment */; } .rtlImageUrlMirroringQuotes1 { - background: url('http://abc.de/img-RTL/fg/hi.png'); - background: /* some comment */ url("http://abc.de/img-RTL/fg/hi.png"); + background: url('http://abc.de/img/fg/hi.png'); + background: /* some comment */ url("http://abc.de/img/fg/hi.png"); background: url('img-RTL/column_header.gif') repeat /* some comment */ scroll 0 0 #d2dae8; background: url("img-RTL/column_header.gif") repeat scroll 0 0 #d2dae8 /* some comment */; } .rtlImageUrlMirroringMultiple1 { - background: url(http://abc.de/img-RTL/fg/hi.png), /* some comment */ url(http://abc.de/img-RTL/fg/hi.png); + background: url(http://abc.de/img/fg/hi.png), /* some comment */ url(http://abc.de/img/fg/hi.png); } .rtlImageUrlMirroringRelative2 { background: url(img-RTL/fg/hi.png) /* some comment */; @@ -39,14 +39,14 @@ list-style-image: url(img-RTL/fg/hi.png); } .rtlImageUrlMirroringAbsolute2 { - background: url(http://abc.de/img-RTL/fg/hi.png) /* some comment */; + background: url(http://abc.de/img/fg/hi.png) /* some comment */; } .rtlImageUrlMirroringQuotes2 { - background: url('http://abc.de/img-RTL/fg/hi.png'); - background: /* some comment */ url("http://abc.de/img-RTL/fg/hi.png"); + background: url('http://abc.de/img/fg/hi.png'); + background: /* some comment */ url("http://abc.de/img/fg/hi.png"); background: url('img-RTL/column_header.gif') repeat /* some comment */ scroll 0 0 #d2dae8; background: url("img-RTL/column_header.gif") repeat scroll 0 0 #d2dae8 /* some comment */; } .rtlImageUrlMirroringMultiple2 { - background: url(http://abc.de/img-RTL/fg/hi.png), /* some comment */ url(http://abc.de/img-RTL/fg/hi.png); + background: url(http://abc.de/img/fg/hi.png), /* some comment */ url(http://abc.de/img/fg/hi.png); } diff --git a/test/fixtures/rtl/background.less b/test/fixtures/rtl/background.less index 4a923fcb..5139f0b4 100644 --- a/test/fixtures/rtl/background.less +++ b/test/fixtures/rtl/background.less @@ -16,8 +16,8 @@ .@{class} { background: red; background: url("chess.png") 40% e("/") 10em #808080 round fixed border-box /* some comment */; - background: url("img/column_header.gif") repeat scroll 0 0 #d2dae8; - background: url("img/column_header.gif") repeat scroll 10% 0 #d2dae8; + background: url("./img/column_header.gif") repeat scroll 0 0 #d2dae8; + background: url("../rtl/img/column_header.gif") repeat scroll 10% 0 #d2dae8; background: url("img/column_header.gif") repeat scroll 0 10% #d2dae8; background: url("chess.png") left top / 10% #808080 round fixed border-box; background: url("chess.png") left 10% e("/") 10% #808080 round fixed border-box; @@ -68,4 +68,4 @@ } } .rtlBackgroundImageMixIn(rtlBackgroundImageMirroringWithMixIn1); -.rtlBackgroundImageMixIn(rtlBackgroundImageMirroringWithMixIn2); \ No newline at end of file +.rtlBackgroundImageMixIn(rtlBackgroundImageMirroringWithMixIn2); diff --git a/test/fixtures/rtl/img-RTL/column_header.gif b/test/fixtures/rtl/img-RTL/column_header.gif new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/rtl/img-RTL/column_header2.gif b/test/fixtures/rtl/img-RTL/column_header2.gif new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/rtl/img-RTL/column_header3.gif b/test/fixtures/rtl/img-RTL/column_header3.gif new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/rtl/img-RTL/drop-down_ico.png b/test/fixtures/rtl/img-RTL/drop-down_ico.png new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/rtl/img-RTL/fg/hi.png b/test/fixtures/rtl/img-RTL/fg/hi.png new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/rtl/img-RTL/hover_column_header.gif b/test/fixtures/rtl/img-RTL/hover_column_header.gif new file mode 100644 index 00000000..e69de29b diff --git a/test/test-url-collector-plugin.js b/test/test-url-collector-plugin.js new file mode 100644 index 00000000..a13b58ca --- /dev/null +++ b/test/test-url-collector-plugin.js @@ -0,0 +1,135 @@ +/* eslint-env mocha */ +"use strict"; + +const assert = require("assert"); +const sinon = require("sinon"); +// const readFile = require("./common/helper").readFile; + +const Builder = require("../lib").Builder; + +// tested module +const UrlCollectorPlugin = require("../lib/plugin/url-collector"); +const RtlPlugin = require("../lib/plugin/rtl"); + +describe("UrlCollectorPlugin", function() { + let getUrlsSpy; let setExistingImgRtlPathsSpy; + beforeEach(function() { + getUrlsSpy = sinon.spy(UrlCollectorPlugin.prototype, "getUrls"); + setExistingImgRtlPathsSpy = sinon.spy(RtlPlugin.prototype, "setExistingImgRtlPaths"); + }); + afterEach(function() { + sinon.restore(); + }); + + it("should return empty array when there are no urls", function() { + return new Builder().build({ + lessInputPath: "test/fixtures/simple/test.less", + }).then(function(result) { + assert.equal(getUrlsSpy.callCount, 1, "UrlCollectorPlugin#getUrls should be called once"); + const getUrlsCall = getUrlsSpy.getCall(0); + const collectedUrls = getUrlsCall.returnValue; + assert.deepEqual(collectedUrls, []); + }); + }); + + it("should collect and resolve all relative urls", function() { + return new Builder().build({ + lessInputPath: "test/fixtures/rtl/background.less", + }).then(function(/* result */) { + assert.equal(getUrlsSpy.callCount, 1, "UrlCollectorPlugin#getUrls should be called once"); + const getUrlsCall = getUrlsSpy.getCall(0); + const collectedUrls = getUrlsCall.returnValue; + assert.deepEqual(collectedUrls, [ + { + relativeUrl: "chess.png", + currentDirectory: "test/fixtures/rtl/" + }, + { + relativeUrl: "img/column_header.gif", + currentDirectory: "test/fixtures/rtl/" + }, + { + relativeUrl: "img/drop-down_ico.png", + currentDirectory: "test/fixtures/rtl/" + }, + { + relativeUrl: "img/hover_column_header.gif", + currentDirectory: "test/fixtures/rtl/" + }, + { + relativeUrl: "img/column_header2.gif", + currentDirectory: "test/fixtures/rtl/" + }, + { + relativeUrl: "img/column_header3.gif", + currentDirectory: "test/fixtures/rtl/" + } + ]); + + assert.equal(setExistingImgRtlPathsSpy.callCount, 1, + "RtlPlugin#setExistingImgRtlPathsSpy should be called once"); + const setExistingImgRtlPathsCall = setExistingImgRtlPathsSpy.getCall(0); + + assert.deepEqual(setExistingImgRtlPathsCall.args, [ + [ + "test/fixtures/rtl/img-RTL/column_header.gif", + "test/fixtures/rtl/img-RTL/drop-down_ico.png", + "test/fixtures/rtl/img-RTL/hover_column_header.gif", + "test/fixtures/rtl/img-RTL/column_header2.gif", + "test/fixtures/rtl/img-RTL/column_header3.gif" + ] + ]); + }); + }); + + it("should not collect data-urls", function() { + return new Builder().build({ + lessInput: ` +.rule { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAAN); +} + `, + }).then(function(/* result */) { + assert.equal(getUrlsSpy.callCount, 1, "UrlCollectorPlugin#getUrls should be called once"); + const getUrlsCall = getUrlsSpy.getCall(0); + const collectedUrls = getUrlsCall.returnValue; + assert.deepEqual(collectedUrls, []); + + assert.equal(setExistingImgRtlPathsSpy.callCount, 1, + "RtlPlugin#setExistingImgRtlPathsSpy should be called once"); + const setExistingImgRtlPathsCall = setExistingImgRtlPathsSpy.getCall(0); + + assert.deepEqual(setExistingImgRtlPathsCall.args, [ + [] + ]); + }); + }); + + it("should not fail on empty url", function() { + return new Builder().build({ + lessInput: ` +.rule { + background-image: url(); +} + `, + }).then(function(/* result */) { + assert.equal(getUrlsSpy.callCount, 1, "UrlCollectorPlugin#getUrls should be called once"); + const getUrlsCall = getUrlsSpy.getCall(0); + const collectedUrls = getUrlsCall.returnValue; + assert.deepEqual(collectedUrls, [ + { + currentDirectory: "", + relativeUrl: "" + } + ]); + + assert.equal(setExistingImgRtlPathsSpy.callCount, 1, + "RtlPlugin#setExistingImgRtlPathsSpy should be called once"); + const setExistingImgRtlPathsCall = setExistingImgRtlPathsSpy.getCall(0); + + assert.deepEqual(setExistingImgRtlPathsCall.args, [ + [] + ]); + }); + }); +}); diff --git a/test/test.js b/test/test.js index 8a3f3eea..cc5a120f 100644 --- a/test/test.js +++ b/test/test.js @@ -2,6 +2,7 @@ "use strict"; const assert = require("assert"); +const sinon = require("sinon"); const path = require("path"); const clone = require("clone"); const readFile = require("./common/helper").readFile; @@ -534,13 +535,9 @@ function assertLessToRtlCssEqual(filename) { const cssFilename = "test/expected/rtl/" + filename + ".css"; return new Builder().build({ - lessInput: readFile(lessFilename), - parser: { - filename: filename + ".less", - paths: "test/fixtures/rtl" - } + lessInputPath: lessFilename }).then(function(result) { - assert.equal(result.cssRtl, readFile(cssFilename), "rtl css should not be generated."); + assert.equal(result.cssRtl, readFile(cssFilename), "rtl css should be generated as expected"); }); } @@ -586,6 +583,107 @@ describe("rtl", function() { }); }); +describe("img-RTL check", function() { + it("Rewrite to img-RTL if file exists", async () => { + const builder = new Builder(); + + const findFileStub = sinon.stub(builder.fileUtils, "findFile"); + findFileStub.rejects(new Error("Unexpected call")) + .withArgs("img-RTL/test.png") + .resolves({path: "img-RTL/test.png", stat: {}}) + .withArgs("foo/img-RTL/noRtlVariant.png") + .resolves(null) + .withArgs("foo/img-RTL/some/image.png") + .resolves({path: "foo/img-RTL/some/image.png", stat: {}}); + + const result = await builder.build({ + lessInput: ` + .rule { + background: url('../img/test.png'); + list-style-image: url('./img/noRtlVariant.png'); /* img-RTL doesn't exist => no rewrite */ + } + .otherRule { + background: url('/absolute/img/test.png'); + /* server-absolute url => no rewrite */ + } + .urlViaVariable { + background: @myUrl; + cursor: @myUrl; + } + @myUrl: url("img/some/image.png"); + `, + parser: { + filename: "foo/bar.less" + }, + cssVariables: true + }); + assert.equal(result.css, `.rule { + background: url('../img/test.png'); + list-style-image: url('img/noRtlVariant.png'); + /* img-RTL doesn't exist => no rewrite */ +} +.otherRule { + background: url('/absolute/img/test.png'); + /* server-absolute url => no rewrite */ +} +.urlViaVariable { + background: url("img/some/image.png"); + cursor: url("img/some/image.png"); +} +`, "css should be generated as expected"); + assert.equal(result.cssRtl, `.rule { + background: url('../img-RTL/test.png'); + list-style-image: url('img/noRtlVariant.png'); + /* img-RTL doesn't exist => no rewrite */ +} +.otherRule { + background: url('/absolute/img/test.png'); + /* server-absolute url => no rewrite */ +} +.urlViaVariable { + background: url("img-RTL/some/image.png"); + cursor: url("img-RTL/some/image.png"); +} +`, "rtl css should be generated as expected"); + + assert.equal(result.cssSkeleton, `.rule { + background: url('../img/test.png'); + list-style-image: url('img/noRtlVariant.png'); + /* img-RTL doesn't exist => no rewrite */ +} +.otherRule { + background: url('/absolute/img/test.png'); + /* server-absolute url => no rewrite */ +} +.urlViaVariable { + background: var(--myUrl); + cursor: var(--myUrl); +} +`, "css skeleton should be generated as expected"); + assert.equal(result.cssSkeletonRtl, `.rule { + background: url('../img-RTL/test.png'); + list-style-image: url('img/noRtlVariant.png'); + /* img-RTL doesn't exist => no rewrite */ +} +.otherRule { + background: url('/absolute/img/test.png'); + /* server-absolute url => no rewrite */ +} +.urlViaVariable { + background: var(--myUrl); + cursor: var(--myUrl); +} +`, "rtl css skeleton should be generated as expected"); + + assert.equal(result.cssVariables, `:root { + --myUrl: url("img/some/image.png"); +} +`, "css variables should be generated as expected"); + + // NOTE: cssVariables are currently LTR only. There is no RTL-variant. + }); +}); + describe("variables", function() { it("should return only globally defined variables", function() { return new Builder().build({