From 81b4035234703ae8b8656cff7d8aef4c93d45031 Mon Sep 17 00:00:00 2001 From: evilebottnawi Date: Wed, 5 Dec 2018 14:52:20 +0300 Subject: [PATCH] fix: unicode characters --- index.js | 99 +++++++++++++++++++++++++--------------------------- package.json | 3 +- test.js | 35 +++++++++++++++++-- 3 files changed, 81 insertions(+), 56 deletions(-) diff --git a/index.js b/index.js index 98a8c09..6c81ff8 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const postcss = require('postcss'); const Tokenizer = require('css-selector-tokenizer'); +const valueParser = require('postcss-value-parser'); function normalizeNodeArray(nodes) { const array = []; @@ -162,45 +163,48 @@ function localizeNode(node, context) { } function localizeDeclNode(node, context) { - let newNode; switch (node.type) { - case 'item': + case 'word': if (context.localizeNextItem) { - newNode = Object.create(node); - newNode.name = ':local(' + newNode.name + ')'; + node.value = ':local(' + node.value + ')'; context.localizeNextItem = false; - return newNode; } break; - case 'nested-item': - const newNodes = node.nodes.map(function(n) { - return localizeDeclValue(n, context); - }); - node = Object.create(node); - node.nodes = newNodes; - break; + case 'function': + if (context.options && context.options.rewriteUrl && node.value.toLowerCase() === 'url') { + node.nodes.map((nestedNode) => { + if (nestedNode.type !== 'string' && nestedNode.type !== 'word') { + return; + } + + let newUrl = context.options.rewriteUrl(context.global, nestedNode.value); + + switch (nestedNode.type) { + case 'string': + if (nestedNode.quote === '\'') { + newUrl = newUrl.replace(/(\\)/g, '\\$1').replace(/'/g, '\\\'') + } + + if (nestedNode.quote === '"') { + newUrl = newUrl.replace(/(\\)/g, '\\$1').replace(/"/g, '\\"') + } + + break; + case 'word': + newUrl = newUrl.replace(/("|'|\)|\\)/g, '\\$1'); + break; + } - case 'url': - if (context.options && context.options.rewriteUrl) { - newNode = Object.create(node); - newNode.url = context.options.rewriteUrl(context.global, node.url); - return newNode; + nestedNode.value = newUrl; + }); } break; } return node; } -function localizeDeclValue(valueNode, context) { - const newValueNode = Object.create(valueNode); - newValueNode.nodes = valueNode.nodes.map(function(node) { - return localizeDeclNode(node, context); - }); - return newValueNode; -} - -function localizeAnimationShorthandDeclValueNodes(nodes, context) { +function localizeAnimationShorthandDeclValues(decl, context) { const validIdent = /^-?[_a-z][_a-z0-9-]*$/i; /* @@ -240,9 +244,9 @@ function localizeAnimationShorthandDeclValueNodes(nodes, context) { const didParseAnimationName = false; const parsedAnimationKeywords = {}; - return nodes.map(function(valueNode) { + const valueNodes = valueParser(decl.value).walk((node) => { const value = - valueNode.type === 'item' ? valueNode.name.toLowerCase() : null; + node.type === 'word' ? node.value.toLowerCase() : null; let shouldParseAnimationName = false; @@ -266,52 +270,43 @@ function localizeAnimationShorthandDeclValueNodes(nodes, context) { global: context.global, localizeNextItem: shouldParseAnimationName && !context.global }; - return localizeDeclNode(valueNode, subContext); + return localizeDeclNode(node, subContext); }); -} -function localizeAnimationShorthandDeclValues(valuesNode, decl, context) { - const newValuesNode = Object.create(valuesNode); - newValuesNode.nodes = valuesNode.nodes.map(function(valueNode, index) { - const newValueNode = Object.create(valueNode); - newValueNode.nodes = localizeAnimationShorthandDeclValueNodes( - valueNode.nodes, - context - ); - return newValueNode; - }); - decl.value = Tokenizer.stringifyValues(newValuesNode); + decl.value = valueNodes.toString(); } -function localizeDeclValues(localize, valuesNode, decl, context) { - const newValuesNode = Object.create(valuesNode); - newValuesNode.nodes = valuesNode.nodes.map(function(valueNode) { +function localizeDeclValues(localize, decl, context) { + const valueNodes = valueParser(decl.value); + valueNodes.walk((node, index, nodes) => { const subContext = { options: context.options, global: context.global, localizeNextItem: localize && !context.global }; - return localizeDeclValue(valueNode, subContext); + nodes[index] = localizeDeclNode(node, subContext); }); - decl.value = Tokenizer.stringifyValues(newValuesNode); + decl.value = valueNodes.toString(); } function localizeDecl(decl, context) { - const valuesNode = Tokenizer.parseValues(decl.value); - - const isAnimation = /animation?$/i.test(decl.prop); + const isAnimation = /animation$/i.test(decl.prop); if (isAnimation) { - return localizeAnimationShorthandDeclValues(valuesNode, decl, context); + return localizeAnimationShorthandDeclValues(decl, context); } const isAnimationName = /animation(-name)?$/i.test(decl.prop); if (isAnimationName) { - return localizeDeclValues(true, valuesNode, decl, context); + return localizeDeclValues(true, decl, context); } - return localizeDeclValues(false, valuesNode, decl, context); + const hasUrl = /url\(/i.test(decl.value); + + if (hasUrl) { + return localizeDeclValues(false, decl, context); + } } module.exports = postcss.plugin('postcss-modules-local-by-default', function( diff --git a/package.json b/package.json index f8158cb..6fa95c6 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ }, "dependencies": { "css-selector-tokenizer": "^0.7.0", - "postcss": "^7.0.6" + "postcss": "^7.0.6", + "postcss-value-parser": "^3.3.1" }, "devDependencies": { "chokidar-cli": "^1.0.1", diff --git a/test.js b/test.js index ca13592..51253cb 100644 --- a/test.js +++ b/test.js @@ -410,12 +410,14 @@ const tests = [ '.a { background: url(./image.png); }\n' + ':global .b { background: url(image.png); }\n' + '.c { background: url("./image.png"); }\n' + + '.c { background: url(\'./image.png\'); }\n' + '.d { background: -webkit-image-set(url("./image.png") 1x, url("./image2x.png") 2x); }\n' + '@font-face { src: url("./font.woff"); }\n' + '@-webkit-font-face { src: url("./font.woff"); }\n' + '@media screen { .a { src: url("./image.png"); } }\n' + '@keyframes :global(ani1) { 0% { src: url("image.png"); } }\n' + - '@keyframes ani2 { 0% { src: url("./image.png"); } }', + '@keyframes ani2 { 0% { src: url("./image.png"); } }\n' + + 'foo { background: end-with-url(something); }', options: { rewriteUrl: function(global, url) { const mode = global ? 'global' : 'local'; @@ -426,12 +428,14 @@ const tests = [ ':local(.a) { background: url((local\\)./image.png\\"local\\"); }\n' + '.b { background: url((global\\)image.png\\"global\\"); }\n' + ':local(.c) { background: url("(local)./image.png\\"local\\""); }\n' + + ':local(.c) { background: url(\'(local)./image.png"local"\'); }\n' + ':local(.d) { background: -webkit-image-set(url("(local)./image.png\\"local\\"") 1x, url("(local)./image2x.png\\"local\\"") 2x); }\n' + '@font-face { src: url("(local)./font.woff\\"local\\""); }\n' + '@-webkit-font-face { src: url("(local)./font.woff\\"local\\""); }\n' + '@media screen { :local(.a) { src: url("(local)./image.png\\"local\\""); } }\n' + '@keyframes ani1 { 0% { src: url("(global)image.png\\"global\\""); } }\n' + - '@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }' + '@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }\n' + + 'foo { background: end-with-url(something); }', }, { should: 'not crash on atrule without nodes', @@ -449,7 +453,32 @@ const tests = [ })(), // postcss-less's stringify would honor `ruleWithoutBody` and omit the trailing `{}` expected: ':local(.a) {\n :local(.b) {}\n}' - } + }, + { + should: 'not break unicode characters', + input: '.a { content: "\\2193" }', + expected: ':local(.a) { content: "\\2193" }' + }, + { + should: 'not break unicode characters', + input: '.a { content: "\\2193\\2193" }', + expected: ':local(.a) { content: "\\2193\\2193" }' + }, + { + should: 'not break unicode characters', + input: '.a { content: "\\2193 \\2193" }', + expected: ':local(.a) { content: "\\2193 \\2193" }' + }, + { + should: 'not break unicode characters', + input: '.a { content: "\\2193\\2193\\2193" }', + expected: ':local(.a) { content: "\\2193\\2193\\2193" }' + }, + { + should: 'not break unicode characters', + input: '.a { content: "\\2193 \\2193 \\2193" }', + expected: ':local(.a) { content: "\\2193 \\2193 \\2193" }' + }, ]; function process(css, options) {