Skip to content

Commit bac0c27

Browse files
committedNov 18, 2019
feat: support nested and multiple invocation
fixes #13
1 parent bb225c5 commit bac0c27

9 files changed

+194
-96
lines changed
 

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"version": "conventional-changelog -i changelog.md -s -r 0 && git commit -am \"build: update changelog\"",
2020
"prepare": "npm run build",
2121
"build": "rimraf lib && babel src -d lib",
22-
"test": "nyc ava"
22+
"test": "nyc ava",
23+
"coverage": "nyc --reporter=lcov --reporter=text-summary ava"
2324
},
2425
"files": [
2526
"lib/"

‎src/constant.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export const METHOD = 'map-get((';
2-
export const CLOSING_PARENTHESIS = ')';

‎src/index.js

+5-33
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,22 @@
11
import postcss from 'postcss';
2-
import strGetContent from './str-get-content';
2+
import processValue from './process-value';
33
import {METHOD} from './constant';
44

5-
const getKeyFromMapString = (mapString, key) => {
6-
// Remove all whitespace character from the key
7-
const keyValue = key.replace(/\s/g, '');
8-
9-
let requiredValue;
10-
11-
mapString.split(',').some(completePropertyString => {
12-
if (completePropertyString.includes(':')) {
13-
const [key, value] = completePropertyString.split(':');
14-
if (key.trim() === keyValue) {
15-
requiredValue = value.trim();
16-
}
17-
} else {
18-
requiredValue = completePropertyString;
19-
}
20-
21-
return Boolean(requiredValue);
22-
});
23-
24-
return requiredValue;
25-
};
26-
27-
const valResolve = val => {
28-
const {before, map, key, after} = strGetContent(val);
29-
30-
return `${before}${getKeyFromMapString(map, key)}${after}`;
31-
};
32-
335
export default postcss.plugin('postcss-map-get', () => {
346
return nodes => {
357
nodes.walkDecls(decl => {
368
let {value} = decl;
379

3810
if (value.includes(METHOD)) {
39-
decl.value = valResolve(decl.value);
11+
decl.value = processValue(decl.value);
4012
}
4113
});
4214

4315
nodes.walkAtRules(rules => {
44-
const {params} = rules;
16+
const {params: parameters} = rules;
4517

46-
if (params.includes(METHOD)) {
47-
rules.params = valResolve(params);
18+
if (parameters.includes(METHOD)) {
19+
rules.params = processValue(parameters);
4820
}
4921
});
5022
};

‎src/parse-parenthesis-content.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Get all the content inside brackets
3+
* @param {string} stringToParse string with parenthesis. This string must start with an `()`
4+
* @param {string} [startingPosition] to start parsing
5+
* @returns {{content: string, position: number}} All the content inside parenthesis including nested properties
6+
*
7+
* @todo might worth to parse the string until a `(` is found?
8+
*/
9+
export default function parseParenthesisContent(stringToParse, startingPosition = 0) {
10+
let position = startingPosition;
11+
let content = '';
12+
13+
const stack = [];
14+
while (true) { // eslint-disable-line no-constant-condition
15+
const mapChracter = stringToParse[position];
16+
if (mapChracter === '(') {
17+
stack.push(mapChracter);
18+
} else if (mapChracter === ')') {
19+
stack.pop();
20+
}
21+
22+
content += mapChracter;
23+
position++; // go ahead
24+
25+
if (stack.length === 0) {
26+
break;
27+
}
28+
}
29+
30+
return {content, position};
31+
}

‎src/process-value.js

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {METHOD} from './constant';
2+
3+
import parseParenthesisContent from './parse-parenthesis-content';
4+
5+
/**
6+
* @param {string} mapString SASS map string without opening parenthesis
7+
* @param {string} keyParameter the key to extract from the map
8+
* @returns {?string} retrieved `keyParameter` value from the map
9+
*/
10+
function getKeyFromMapString(mapString, keyParameter) {
11+
// remove all whitespace character from the key
12+
const keyValue = keyParameter.replace(/\s/g, '');
13+
// remove open and close parenthesis from the map string
14+
mapString = mapString.slice(1, -1);
15+
16+
const map = {};
17+
18+
let isParsingKey = true;
19+
20+
let key = '';
21+
let value = '';
22+
23+
for (let position = 0; position < mapString.length; position++) {
24+
const currentCharacter = mapString[position];
25+
26+
if (isParsingKey) { // process the key (add all characters until find a :)
27+
if (currentCharacter === ':') {
28+
isParsingKey = false;
29+
} else {
30+
key += currentCharacter;
31+
}
32+
} else if (currentCharacter === '(') {
33+
// if value contains a `(` that means that is map so parse the string until the `(` is closed
34+
const output = parseParenthesisContent(mapString, position);
35+
value += output.content;
36+
position = output.position;
37+
38+
map[key] = value.trim();
39+
40+
// value declaration is complete return to check key and reset both variables
41+
isParsingKey = true;
42+
key = '';
43+
value = '';
44+
} else {
45+
// simple map with property / value pairs
46+
const isLastCharacter = position === mapString.length - 1;
47+
if (currentCharacter === ',' || isLastCharacter) { // map with only one property or end of the string
48+
if (isLastCharacter) {
49+
value += currentCharacter;
50+
}
51+
52+
map[key] = value.trim();
53+
54+
isParsingKey = true;
55+
key = '';
56+
value = '';
57+
} else {
58+
value += currentCharacter;
59+
}
60+
}
61+
}
62+
63+
return map[keyValue];
64+
}
65+
66+
/**
67+
* @param {string} value CSS property value including map-get invocation
68+
* @returns {string} value of css property resolved by map-get
69+
*/
70+
export default function (value) {
71+
let resolvedValue = value;
72+
// start to resolve map-get more nested
73+
let indexOfMethod = resolvedValue.indexOf(METHOD);
74+
while (indexOfMethod > -1) {
75+
const startPosition = indexOfMethod;
76+
let position = (startPosition + METHOD.length) - 1;
77+
78+
let mapString = '';
79+
80+
// resolve map content
81+
const output = parseParenthesisContent(resolvedValue, position);
82+
mapString += output.content;
83+
position = output.position;
84+
85+
// resolve the desidered requested key
86+
let keyString = '';
87+
let hasFoundComa = false;
88+
89+
for (; position < resolvedValue.length; position++) {
90+
const currentCharacter = resolvedValue[position];
91+
92+
if (currentCharacter === ',') {
93+
hasFoundComa = true;
94+
} else if (currentCharacter === ')') {
95+
break;
96+
} else if (hasFoundComa) {
97+
keyString += currentCharacter;
98+
}
99+
}
100+
101+
// get the original invocation string
102+
position++; // Include last closing parenthesis
103+
const currentDeclaration = resolvedValue.slice(startPosition, position);
104+
105+
const mapResolvedValue = getKeyFromMapString(mapString, keyString);
106+
107+
// replace the value string with the resolved value
108+
resolvedValue = resolvedValue.replace(currentDeclaration, mapResolvedValue);
109+
110+
// check if property value contains another map-get invocation
111+
indexOfMethod = resolvedValue.indexOf(METHOD);
112+
}
113+
114+
return resolvedValue;
115+
}

‎src/str-get-content.js

-30
This file was deleted.
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import test from 'ava';
2+
import parseParenthesisContent from '../src/parse-parenthesis-content';
3+
4+
const tests = [
5+
{
6+
stringToParse: '(foo: bar) something else',
7+
expectedContent: '(foo: bar)'
8+
},
9+
{
10+
stringToParse: '(foo: bar, asd: (innerFoo: innerBar)) something else',
11+
expectedContent: '(foo: bar, asd: (innerFoo: innerBar))'
12+
},
13+
{
14+
stringToParse: '(foo: bar, asd: (innerFoo: (deepFoo: deepBar))) something else',
15+
expectedContent: '(foo: bar, asd: (innerFoo: (deepFoo: deepBar)))'
16+
},
17+
{
18+
stringToParse: 'before(foo: bar, asd: (innerFoo: (deepFoo: deepBar))) something else',
19+
startPosition: 6,
20+
expectedContent: '(foo: bar, asd: (innerFoo: (deepFoo: deepBar)))'
21+
}
22+
];
23+
24+
tests.forEach(({stringToParse, startPosition, expectedContent}, testIndex) => {
25+
test(`parse-parenthesis-content ${testIndex} – "${stringToParse}" should result in "${expectedContent}"`, function (t) {
26+
const output = parseParenthesisContent(stringToParse, startPosition);
27+
28+
t.deepEqual(output.content, expectedContent);
29+
30+
const expectedPosition = stringToParse.indexOf(expectedContent) + expectedContent.length;
31+
t.deepEqual(output.position, expectedPosition);
32+
});
33+
});

‎test/test.plugin.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,14 @@ test('it should keep proper string format for element before invocation, borderS
4242
t.is(processing(value), expected);
4343
});
4444

45-
test('multi getters property', t => {
46-
const expected = '.foo {color: "green"}';
47-
const value = '.foo {color: map-get(map-get((corporate: (textColor: "green"), ea: (textColor: "black")), corporate), textColor)}';
45+
test('it should resolve multiple map-get on the same property value', t => {
46+
const expected = '.foo {border: 1px solid #FFF;}';
47+
const value = '.foo {border: 1px map-get((borderStyle: solid), borderStyle) map-get((borderColor: #FFF), borderColor);}';
4848
t.is(processing(value), expected);
4949
});
5050

51+
test('it should resolve nested invocation', t => {
52+
const expected = '.foo {color: green}';
53+
const value = '.foo {color: map-get(map-get((corporate: (textColor: green), ea: (textColor: black)), corporate), textColor)}';
54+
t.is(processing(value), expected);
55+
});

‎test/test.str-get-content.js

-28
This file was deleted.

0 commit comments

Comments
 (0)