Skip to content

Commit 61ac715

Browse files
committed
Refactor to use external parser
1 parent 9786793 commit 61ac715

File tree

10 files changed

+359
-34
lines changed

10 files changed

+359
-34
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"dependencies": {
3838
"css-color-list": "0.0.1",
3939
"fbjs": "^0.8.5",
40-
"nearley": "^2.7.7"
40+
"nearley": "^2.7.7",
41+
"postcss-values-parser": "^1.0.1"
4142
}
4243
}

src/index.js

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,23 @@
11
/* eslint-disable no-param-reassign */
2-
const nearley = require('nearley');
2+
const parser = require('postcss-values-parser/lib/index');
33
const camelizeStyleName = require('fbjs/lib/camelizeStyleName');
4-
const grammar = require('./grammar');
5-
6-
const transforms = [
7-
'background',
8-
'border',
9-
'borderColor',
10-
'borderRadius',
11-
'borderWidth',
12-
'flex',
13-
'flexFlow',
14-
'font',
15-
'fontVariant',
16-
'fontWeight',
17-
'margin',
18-
'padding',
19-
'shadowOffset',
20-
'textShadowOffset',
21-
'transform',
22-
];
4+
const transforms = require('./transforms');
235

246
const transformRawValue = input => (
257
(input !== '' && !isNaN(input))
268
? Number(input)
279
: input
2810
);
2911

30-
export const parseProp = (propName, value) =>
31-
new nearley.Parser(grammar.ParserRules, propName).feed(value).results[0];
12+
export const parseProp = (propName, value) => {
13+
const ast = parser(value).parse();
14+
return transforms[propName](ast);
15+
};
3216

3317
export const getStylesForProperty = (propName, inputValue, allowShorthand) => {
3418
const value = inputValue.trim();
3519

36-
const propValue = (allowShorthand && transforms.indexOf(propName) !== -1)
20+
const propValue = (allowShorthand && (propName in transforms))
3721
? parseProp(propName, value)
3822
: transformRawValue(value);
3923

src/index.test.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ it('transforms numbers', () => runTest([
1414
], { top: 0, left: 0, right: 0, bottom: 0 }));
1515

1616
it('allows decimal values', () => {
17-
expect(parseProp('number', '0.5')).toBe(0.5);
18-
expect(parseProp('number', '1.5')).toBe(1.5);
19-
expect(parseProp('number', '10.5')).toBe(10.5);
20-
expect(parseProp('number', '100.5')).toBe(100.5);
21-
expect(parseProp('number', '-0.5')).toBe(-0.5);
22-
expect(parseProp('number', '-1.5')).toBe(-1.5);
23-
expect(parseProp('number', '-10.5')).toBe(-10.5);
24-
expect(parseProp('number', '-100.5')).toBe(-100.5);
25-
expect(parseProp('number', '.5')).toBe(0.5);
26-
expect(parseProp('number', '-.5')).toBe(-0.5);
17+
expect(parseProp('margin', '0.5').$merge.marginTop).toBe(0.5);
18+
expect(parseProp('margin', '1.5').$merge.marginTop).toBe(1.5);
19+
expect(parseProp('margin', '10.5').$merge.marginTop).toBe(10.5);
20+
expect(parseProp('margin', '100.5').$merge.marginTop).toBe(100.5);
21+
expect(parseProp('margin', '-0.5').$merge.marginTop).toBe(-0.5);
22+
expect(parseProp('margin', '-1.5').$merge.marginTop).toBe(-1.5);
23+
expect(parseProp('margin', '-10.5').$merge.marginTop).toBe(-10.5);
24+
expect(parseProp('margin', '-100.5').$merge.marginTop).toBe(-100.5);
25+
expect(parseProp('margin', '.5').$merge.marginTop).toBe(0.5);
26+
expect(parseProp('margin', '-.5').$merge.marginTop).toBe(-0.5);
2727
});
2828

2929
it('allows decimal values in transformed values', () => runTest([

src/transforms/border.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* eslint-disable no-param-reassign */
2+
const defaultWidth = 1;
3+
const defaultStyle = 'solid';
4+
const defaultColor = 'black';
5+
6+
const styles = ['solid', 'dotted', 'dashed'];
7+
8+
module.exports = (root) => {
9+
const { nodes } = root.first;
10+
const values = nodes.reduce((accum, node) => {
11+
if (accum.width === undefined && node.type === 'number') {
12+
accum.width = Number(node.value);
13+
} else if (accum.style === undefined && node.type === 'word' && styles.indexOf(node.value) !== -1) {
14+
accum.style = node.value;
15+
} else if (accum.color === undefined && node.type === 'word' && node.isColor) {
16+
accum.color = node.value;
17+
} else {
18+
throw new Error(`Unexpected value: ${node}`);
19+
}
20+
return accum;
21+
}, {
22+
width: undefined,
23+
style: undefined,
24+
color: undefined,
25+
});
26+
27+
const {
28+
width: borderWidth = defaultWidth,
29+
style: borderStyle = defaultStyle,
30+
color: borderColor = defaultColor,
31+
} = values;
32+
33+
return { $merge: { borderWidth, borderStyle, borderColor } };
34+
};

src/transforms/flex.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const { assertUptoNValuesOfType } = require('./util');
2+
3+
module.exports = (root) => {
4+
const { nodes } = root.first;
5+
assertUptoNValuesOfType(3, 'number', nodes);
6+
7+
const [flexGrow, flexShrink = 1, flexBasis = 0] = nodes.map(node => Number(node.value));
8+
9+
return { $merge: { flexGrow, flexShrink, flexBasis } };
10+
};

src/transforms/flexFlow.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* eslint-disable no-param-reassign */
2+
const { assertUptoNValuesOfType } = require('./util');
3+
4+
const defaultWrap = 'nowrap';
5+
const defaultDirection = 'row';
6+
7+
const wraps = ['nowrap', 'wrap', 'wrap-reverse'];
8+
const directions = ['row', 'row-reverse', 'column', 'column-reverse'];
9+
10+
module.exports = (root) => {
11+
const { nodes } = root.first;
12+
assertUptoNValuesOfType(2, 'word', nodes);
13+
14+
const values = nodes.reduce((accum, node) => {
15+
if (accum.wrap === undefined && wraps.indexOf(node.value) !== -1) {
16+
accum.wrap = node.value;
17+
} else if (accum.direction === undefined && directions.indexOf(node.value) !== -1) {
18+
accum.direction = node.value;
19+
} else {
20+
throw new Error(`Unexpected value: ${node}`);
21+
}
22+
return accum;
23+
}, {
24+
wrap: undefined,
25+
direction: undefined,
26+
});
27+
28+
const {
29+
wrap: flexWrap = defaultWrap,
30+
direction: flexDirection = defaultDirection,
31+
} = values;
32+
33+
return { $merge: { flexWrap, flexDirection } };
34+
};

src/transforms/font.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/* eslint-disable no-param-reassign */
2+
const PARSE_STYLE_WEIGHT_VARIANT = 0;
3+
const PARSE_SIZE = 1;
4+
const PARSE_MAYBE_LINE_HEIGHT = 2;
5+
const PARSE_LINE_HEIGHT = 3;
6+
const PARSE_FONT_FAMILY = 4;
7+
const PARSE_FINISHED = 5;
8+
9+
const styles = ['italic'];
10+
const weights = ['bold'];
11+
const numericWeights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
12+
const variants = ['small-caps'];
13+
14+
module.exports = (root) => {
15+
const { nodes } = root.first;
16+
17+
const values = nodes.reduce((accum, node) => {
18+
if (accum.parseMode === PARSE_STYLE_WEIGHT_VARIANT) {
19+
let didMatchStyleWeightVariant = true;
20+
const { type, value } = node;
21+
22+
if (type === 'word' && value === 'normal') {
23+
/* pass */
24+
} else if (accum.style === undefined && type === 'word' && styles.indexOf(value) !== -1) {
25+
accum.style = value;
26+
} else if (accum.weight === undefined && type === 'number' && numericWeights.indexOf(Number(value)) !== -1) {
27+
accum.weight = String(value);
28+
} else if (accum.weight === undefined && type === 'word' && weights.indexOf(value) !== -1) {
29+
accum.weight = value;
30+
} else if (accum.variant === undefined && type === 'word' && variants.indexOf(value) !== -1) {
31+
accum.variant = [value];
32+
} else {
33+
didMatchStyleWeightVariant = false;
34+
}
35+
36+
if (didMatchStyleWeightVariant) {
37+
accum.numStyleWeightVariantMatched += 1;
38+
if (accum.numStyleWeightVariantMatched === 3) accum.parseMode = PARSE_SIZE;
39+
return accum;
40+
}
41+
42+
accum.parseMode = PARSE_SIZE; // fallthrough
43+
}
44+
45+
if (accum.parseMode === PARSE_SIZE) {
46+
if (accum.size === undefined && node.type === 'number') {
47+
accum.size = Number(node.value);
48+
accum.parseMode = PARSE_MAYBE_LINE_HEIGHT;
49+
return accum;
50+
}
51+
}
52+
53+
if (accum.parseMode === PARSE_MAYBE_LINE_HEIGHT) {
54+
if (node.type === 'operator' && node.value === '/') {
55+
accum.parseMode = PARSE_LINE_HEIGHT;
56+
return accum;
57+
}
58+
accum.parseMode = PARSE_FONT_FAMILY; // fallthrough
59+
}
60+
61+
if (accum.parseMode === PARSE_LINE_HEIGHT) {
62+
if (accum.lineHeight === undefined && node.type === 'number') {
63+
accum.lineHeight = Number(node.value);
64+
accum.parseMode = PARSE_FONT_FAMILY;
65+
return accum;
66+
}
67+
}
68+
69+
if (accum.parseMode === PARSE_FONT_FAMILY) {
70+
if (accum.family === undefined && node.type === 'string') {
71+
accum.family = node.value;
72+
accum.parseMode = PARSE_FINISHED;
73+
return accum;
74+
} else if (node.type === 'word') {
75+
accum.family = `${(accum.family || '')} ${node.value}`;
76+
return accum;
77+
}
78+
}
79+
80+
throw new Error(`Unexpected value: ${node}`);
81+
}, {
82+
numStyleWeightVariantMatched: 0,
83+
parseMode: PARSE_STYLE_WEIGHT_VARIANT,
84+
style: undefined,
85+
weight: undefined,
86+
variant: undefined,
87+
size: undefined,
88+
lineHeight: undefined,
89+
family: undefined,
90+
});
91+
92+
const {
93+
style: fontStyle = 'normal',
94+
weight: fontWeight = 'normal',
95+
variant: fontVariant = [],
96+
size: fontSize,
97+
family: fontFamily,
98+
} = values;
99+
100+
if (fontSize === undefined || fontFamily === undefined) throw new Error('Unexpected error');
101+
102+
const out = { fontStyle, fontWeight, fontVariant, fontSize, fontFamily };
103+
if (values.lineHeight !== undefined) out.lineHeight = values.lineHeight;
104+
105+
return { $merge: out };
106+
};

src/transforms/index.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const border = require('./border');
2+
const flex = require('./flex');
3+
const flexFlow = require('./flexFlow');
4+
const font = require('./font');
5+
const transform = require('./transform');
6+
const { directionFactory, shadowOffsetFactory } = require('./util');
7+
8+
const background = root => ({ $merge: { backgroundColor: String(root) } });
9+
const borderColor = directionFactory({ type: 'word', prefix: 'border', suffix: 'Color' });
10+
const borderRadius = directionFactory({
11+
directions: ['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft'],
12+
prefix: 'border',
13+
suffix: 'Radius',
14+
});
15+
const borderWidth = directionFactory({ prefix: 'border', suffix: 'Width' });
16+
const margin = directionFactory({ prefix: 'margin' });
17+
const padding = directionFactory({ prefix: 'padding' });
18+
const fontVariant = root => root.first.nodes.map(String);
19+
const fontWeight = root => String(root);
20+
const shadowOffset = shadowOffsetFactory('textShadowOffset');
21+
const textShadowOffset = shadowOffsetFactory();
22+
23+
// const transforms = [
24+
// 'background',
25+
// 'border',
26+
// 'borderColor',
27+
// 'borderRadius',
28+
// 'borderWidth',
29+
// 'flex',
30+
// 'flexFlow',
31+
// 'font',
32+
// 'fontVariant',
33+
// 'fontWeight',
34+
// 'margin',
35+
// 'padding',
36+
// 'shadowOffset',
37+
// 'textShadowOffset',
38+
// 'transform',
39+
// ];
40+
41+
module.exports = {
42+
background,
43+
border,
44+
borderColor,
45+
borderRadius,
46+
borderWidth,
47+
flex,
48+
flexFlow,
49+
font,
50+
fontVariant,
51+
fontWeight,
52+
margin,
53+
padding,
54+
shadowOffset,
55+
textShadowOffset,
56+
transform,
57+
};

src/transforms/transform.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const { assertUptoNValuesOfType } = require('./util');
2+
3+
const singleNumber = nodes => Number(nodes[1].value);
4+
const singleAngle = nodes => String(nodes[1]);
5+
const xyTransformFactory = transform => (key, valueIfOmitted) => (nodes) => {
6+
const [
7+
/* paren */,
8+
xValue,
9+
/* comma */,
10+
yValue,
11+
] = nodes;
12+
13+
if (xValue.type !== 'number' || (yValue && yValue.type !== 'number')) {
14+
throw new Error('Expected values to be numbers');
15+
}
16+
17+
const x = transform(xValue);
18+
19+
if (valueIfOmitted === undefined && yValue === undefined) return x;
20+
21+
const y = yValue !== undefined ? transform(yValue) : valueIfOmitted;
22+
return [{ [`${key}Y`]: y }, { [`${key}X`]: x }];
23+
};
24+
const xyNumber = xyTransformFactory(node => Number(node.value));
25+
const xyAngle = xyTransformFactory(node => String(node).trim());
26+
27+
const partTransforms = {
28+
perspective: singleNumber,
29+
scale: xyNumber('scale'),
30+
scaleX: singleNumber,
31+
scaleY: singleNumber,
32+
translate: xyNumber('translate', 0),
33+
translateX: singleNumber,
34+
translateY: singleNumber,
35+
rotate: singleAngle,
36+
rotateX: singleAngle,
37+
rotateY: singleAngle,
38+
rotateZ: singleAngle,
39+
skewX: singleAngle,
40+
skewY: singleAngle,
41+
skew: xyAngle('skew', '0deg'),
42+
};
43+
44+
module.exports = (root) => {
45+
const { nodes } = root.first;
46+
assertUptoNValuesOfType(Infinity, 'func', nodes);
47+
48+
const transforms = nodes.reduce((accum, node) => {
49+
if (!(node.value in partTransforms)) throw new Error(`Unrecognised transform: ${node.value}`);
50+
51+
let transformedValues = partTransforms[node.value](node.nodes);
52+
if (!Array.isArray(transformedValues)) {
53+
transformedValues = [{ [node.value]: transformedValues }];
54+
}
55+
56+
return transformedValues.concat(accum);
57+
}, []);
58+
59+
return transforms;
60+
};

0 commit comments

Comments
 (0)