Skip to content

Commit 83c87d2

Browse files
fix: unicode characters (#4)
1 parent a0128ed commit 83c87d2

File tree

3 files changed

+81
-56
lines changed

3 files changed

+81
-56
lines changed

index.js

Lines changed: 47 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const postcss = require('postcss');
44
const Tokenizer = require('css-selector-tokenizer');
5+
const valueParser = require('postcss-value-parser');
56

67
function normalizeNodeArray(nodes) {
78
const array = [];
@@ -162,45 +163,48 @@ function localizeNode(node, context) {
162163
}
163164

164165
function localizeDeclNode(node, context) {
165-
let newNode;
166166
switch (node.type) {
167-
case 'item':
167+
case 'word':
168168
if (context.localizeNextItem) {
169-
newNode = Object.create(node);
170-
newNode.name = ':local(' + newNode.name + ')';
169+
node.value = ':local(' + node.value + ')';
171170
context.localizeNextItem = false;
172-
return newNode;
173171
}
174172
break;
175173

176-
case 'nested-item':
177-
const newNodes = node.nodes.map(function(n) {
178-
return localizeDeclValue(n, context);
179-
});
180-
node = Object.create(node);
181-
node.nodes = newNodes;
182-
break;
174+
case 'function':
175+
if (context.options && context.options.rewriteUrl && node.value.toLowerCase() === 'url') {
176+
node.nodes.map((nestedNode) => {
177+
if (nestedNode.type !== 'string' && nestedNode.type !== 'word') {
178+
return;
179+
}
180+
181+
let newUrl = context.options.rewriteUrl(context.global, nestedNode.value);
182+
183+
switch (nestedNode.type) {
184+
case 'string':
185+
if (nestedNode.quote === '\'') {
186+
newUrl = newUrl.replace(/(\\)/g, '\\$1').replace(/'/g, '\\\'')
187+
}
188+
189+
if (nestedNode.quote === '"') {
190+
newUrl = newUrl.replace(/(\\)/g, '\\$1').replace(/"/g, '\\"')
191+
}
192+
193+
break;
194+
case 'word':
195+
newUrl = newUrl.replace(/("|'|\)|\\)/g, '\\$1');
196+
break;
197+
}
183198

184-
case 'url':
185-
if (context.options && context.options.rewriteUrl) {
186-
newNode = Object.create(node);
187-
newNode.url = context.options.rewriteUrl(context.global, node.url);
188-
return newNode;
199+
nestedNode.value = newUrl;
200+
});
189201
}
190202
break;
191203
}
192204
return node;
193205
}
194206

195-
function localizeDeclValue(valueNode, context) {
196-
const newValueNode = Object.create(valueNode);
197-
newValueNode.nodes = valueNode.nodes.map(function(node) {
198-
return localizeDeclNode(node, context);
199-
});
200-
return newValueNode;
201-
}
202-
203-
function localizeAnimationShorthandDeclValueNodes(nodes, context) {
207+
function localizeAnimationShorthandDeclValues(decl, context) {
204208
const validIdent = /^-?[_a-z][_a-z0-9-]*$/i;
205209

206210
/*
@@ -240,9 +244,9 @@ function localizeAnimationShorthandDeclValueNodes(nodes, context) {
240244

241245
const didParseAnimationName = false;
242246
const parsedAnimationKeywords = {};
243-
return nodes.map(function(valueNode) {
247+
const valueNodes = valueParser(decl.value).walk((node) => {
244248
const value =
245-
valueNode.type === 'item' ? valueNode.name.toLowerCase() : null;
249+
node.type === 'word' ? node.value.toLowerCase() : null;
246250

247251
let shouldParseAnimationName = false;
248252

@@ -266,52 +270,43 @@ function localizeAnimationShorthandDeclValueNodes(nodes, context) {
266270
global: context.global,
267271
localizeNextItem: shouldParseAnimationName && !context.global
268272
};
269-
return localizeDeclNode(valueNode, subContext);
273+
return localizeDeclNode(node, subContext);
270274
});
271-
}
272275

273-
function localizeAnimationShorthandDeclValues(valuesNode, decl, context) {
274-
const newValuesNode = Object.create(valuesNode);
275-
newValuesNode.nodes = valuesNode.nodes.map(function(valueNode, index) {
276-
const newValueNode = Object.create(valueNode);
277-
newValueNode.nodes = localizeAnimationShorthandDeclValueNodes(
278-
valueNode.nodes,
279-
context
280-
);
281-
return newValueNode;
282-
});
283-
decl.value = Tokenizer.stringifyValues(newValuesNode);
276+
decl.value = valueNodes.toString();
284277
}
285278

286-
function localizeDeclValues(localize, valuesNode, decl, context) {
287-
const newValuesNode = Object.create(valuesNode);
288-
newValuesNode.nodes = valuesNode.nodes.map(function(valueNode) {
279+
function localizeDeclValues(localize, decl, context) {
280+
const valueNodes = valueParser(decl.value);
281+
valueNodes.walk((node, index, nodes) => {
289282
const subContext = {
290283
options: context.options,
291284
global: context.global,
292285
localizeNextItem: localize && !context.global
293286
};
294-
return localizeDeclValue(valueNode, subContext);
287+
nodes[index] = localizeDeclNode(node, subContext);
295288
});
296-
decl.value = Tokenizer.stringifyValues(newValuesNode);
289+
decl.value = valueNodes.toString();
297290
}
298291

299292
function localizeDecl(decl, context) {
300-
const valuesNode = Tokenizer.parseValues(decl.value);
301-
302-
const isAnimation = /animation?$/i.test(decl.prop);
293+
const isAnimation = /animation$/i.test(decl.prop);
303294

304295
if (isAnimation) {
305-
return localizeAnimationShorthandDeclValues(valuesNode, decl, context);
296+
return localizeAnimationShorthandDeclValues(decl, context);
306297
}
307298

308299
const isAnimationName = /animation(-name)?$/i.test(decl.prop);
309300

310301
if (isAnimationName) {
311-
return localizeDeclValues(true, valuesNode, decl, context);
302+
return localizeDeclValues(true, decl, context);
312303
}
313304

314-
return localizeDeclValues(false, valuesNode, decl, context);
305+
const hasUrl = /url\(/i.test(decl.value);
306+
307+
if (hasUrl) {
308+
return localizeDeclValues(false, decl, context);
309+
}
315310
}
316311

317312
module.exports = postcss.plugin('postcss-modules-local-by-default', function(

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
},
2121
"dependencies": {
2222
"css-selector-tokenizer": "^0.7.0",
23-
"postcss": "^7.0.6"
23+
"postcss": "^7.0.6",
24+
"postcss-value-parser": "^3.3.1"
2425
},
2526
"devDependencies": {
2627
"chokidar-cli": "^1.0.1",

test.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,14 @@ const tests = [
410410
'.a { background: url(./image.png); }\n' +
411411
':global .b { background: url(image.png); }\n' +
412412
'.c { background: url("./image.png"); }\n' +
413+
'.c { background: url(\'./image.png\'); }\n' +
413414
'.d { background: -webkit-image-set(url("./image.png") 1x, url("./image2x.png") 2x); }\n' +
414415
'@font-face { src: url("./font.woff"); }\n' +
415416
'@-webkit-font-face { src: url("./font.woff"); }\n' +
416417
'@media screen { .a { src: url("./image.png"); } }\n' +
417418
'@keyframes :global(ani1) { 0% { src: url("image.png"); } }\n' +
418-
'@keyframes ani2 { 0% { src: url("./image.png"); } }',
419+
'@keyframes ani2 { 0% { src: url("./image.png"); } }\n' +
420+
'foo { background: end-with-url(something); }',
419421
options: {
420422
rewriteUrl: function(global, url) {
421423
const mode = global ? 'global' : 'local';
@@ -426,12 +428,14 @@ const tests = [
426428
':local(.a) { background: url((local\\)./image.png\\"local\\"); }\n' +
427429
'.b { background: url((global\\)image.png\\"global\\"); }\n' +
428430
':local(.c) { background: url("(local)./image.png\\"local\\""); }\n' +
431+
':local(.c) { background: url(\'(local)./image.png"local"\'); }\n' +
429432
':local(.d) { background: -webkit-image-set(url("(local)./image.png\\"local\\"") 1x, url("(local)./image2x.png\\"local\\"") 2x); }\n' +
430433
'@font-face { src: url("(local)./font.woff\\"local\\""); }\n' +
431434
'@-webkit-font-face { src: url("(local)./font.woff\\"local\\""); }\n' +
432435
'@media screen { :local(.a) { src: url("(local)./image.png\\"local\\""); } }\n' +
433436
'@keyframes ani1 { 0% { src: url("(global)image.png\\"global\\""); } }\n' +
434-
'@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }'
437+
'@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }\n' +
438+
'foo { background: end-with-url(something); }',
435439
},
436440
{
437441
should: 'not crash on atrule without nodes',
@@ -449,7 +453,32 @@ const tests = [
449453
})(),
450454
// postcss-less's stringify would honor `ruleWithoutBody` and omit the trailing `{}`
451455
expected: ':local(.a) {\n :local(.b) {}\n}'
452-
}
456+
},
457+
{
458+
should: 'not break unicode characters',
459+
input: '.a { content: "\\2193" }',
460+
expected: ':local(.a) { content: "\\2193" }'
461+
},
462+
{
463+
should: 'not break unicode characters',
464+
input: '.a { content: "\\2193\\2193" }',
465+
expected: ':local(.a) { content: "\\2193\\2193" }'
466+
},
467+
{
468+
should: 'not break unicode characters',
469+
input: '.a { content: "\\2193 \\2193" }',
470+
expected: ':local(.a) { content: "\\2193 \\2193" }'
471+
},
472+
{
473+
should: 'not break unicode characters',
474+
input: '.a { content: "\\2193\\2193\\2193" }',
475+
expected: ':local(.a) { content: "\\2193\\2193\\2193" }'
476+
},
477+
{
478+
should: 'not break unicode characters',
479+
input: '.a { content: "\\2193 \\2193 \\2193" }',
480+
expected: ':local(.a) { content: "\\2193 \\2193 \\2193" }'
481+
},
453482
];
454483

455484
function process(css, options) {

0 commit comments

Comments
 (0)