diff --git a/src/grammar.ne b/src/grammar.ne index debbc08..44f30c4 100644 --- a/src/grammar.ne +++ b/src/grammar.ne @@ -4,6 +4,7 @@ const at = index => d => d && d[index]; const pick = indices => d => indices.map(index => d[index]); const text = d => Array.isArray(d) ? d.map(text).join('') : d; + const getCharCode = d => String.fromCharCode(parseInt(text(d[1]), 16)); const transformArg1 = d => ({ [d[0].join('')]: d[2][0] }); const defaultOptional = (value, defaultValue) => value === null ? defaultValue : value; @@ -55,24 +56,48 @@ int decimal -> "." [0-9]:+ +numberToken + -> [+-]:? (int decimal | int | decimal) ([Ee] [+-]:? int):? {% text %} + number - -> "-":? (int decimal | int | decimal) {% d => Number(text(d)) %} + -> numberToken {% Number %} + +percent + -> numberToken "%" {% text %} + +numberOrPercent + -> percent {% id %} + | number {% id %} + +hex -> [a-fA-F0-9] {% text %} angle -> number ("deg" | "rad") {% text %} ident -> ("-":? [_A-Za-z] [_A-Za-z0-9-]:*) {% text %} -# ident -> [^ ]:+ {% text %} color - -> "#" ([a-fA-F0-9]:*) {% text %} + -> "#" (hex:*) {% text %} | ("rgb" | "hsl" | "hsv") ("a":?) "(" ([^)]:+) ")" {% text %} | ([A-Za-z]:+) {% (d, location, reject) => { const name = text(d).toLowerCase(); return cssColorList.indexOf(name) !== -1 ? name : reject; } %} -_ -> [ \t\n\r]:* {% () => null %} -__ -> [ \t\n\r]:+ {% () => null %} +escapeChar + -> "\\" (hex) " " {% getCharCode %} + | "\\" (hex hex) " " {% getCharCode %} + | "\\" (hex hex hex) " " {% getCharCode %} + | "\\" (hex hex hex hex) " " {% getCharCode %} + | "\\" (hex hex hex hex hex) " " {% getCharCode %} + | "\\" (hex hex hex hex hex hex) {% getCharCode %} + | "\\" [^a-fA-F0-9] {% d => text(d[1]) %} + +string + -> "\"" (escapeChar | [^"\\]):* "\"" {% d => text(d[1]) %} + | "'" (escapeChar | [^'\\]):* "'" {% d => text(d[1]) %} + +_ -> [ \t\n\r]:* {% text %} +__ -> [ \t\n\r]:+ {% text %} anyOrder2[a, b] -> $a __ $b {% d => [d[0][0][0], d[2][0][0]] %} @@ -163,24 +188,39 @@ flexFlow } }) %} flex - -> number (__ number):* {% (d, location, reject) => { - const values = combineHeadTail(d); - if (values.length > 3) return reject; - const [flexGrow, flexShrink = 1, flexBasis = 0] = values; + -> number __ number __ numberOrPercent {% (d, location, reject) => { + const [flexGrow, /* whitespace */, flexShrink, /* whitespace */, flexBasis] = d; return { $merge: { flexGrow, flexShrink, flexBasis } }; } %} + | number __ number {% (d, location, reject) => { + const [flexGrow, /* whitespace */, flexShrink] = d; + return { $merge: { flexGrow, flexShrink, flexBasis: 0 } }; + } %} + | number __ percent {% (d, location, reject) => { + const [flexGrow, /* whitespace */, flexBasis] = d; + return { $merge: { flexGrow, flexShrink: 1, flexBasis } }; + } %} + | number {% (d, location, reject) => { + const [flexGrow] = d; + return { $merge: { flexGrow, flexShrink: 1, flexBasis: 0 } }; + } %} + | percent {% (d, location, reject) => { + const [flexBasis] = d; + return { $merge: { flexGrow: 1, flexShrink: 1, flexBasis } }; + } %} + +fontFamily + -> string {% at(0) %} + | (ident (_ ident):*) {% text %} fontFontStyle -> ("normal" | "italic") {% text %} fontFontVariantCss21 -> "normal" {% () => [] %} | "small-caps" {% () => ['small-caps'] %} fontFontWeight -> ("normal" | "bold" | [1-9] "00") {% text %} -fontFontFamily - -> "\"" ("\\" . | [^"]):* "\"" {% d => text(d[1]) %} - | "'" ("\\" . | [^']):* "'" {% d => text(d[1]) %} font -> anyOrderOptional3AllowNull[fontFontStyle, fontFontVariantCss21, fontFontWeight] _ number (_ "/" _ number):? __ - fontFontFamily {% d => { + fontFamily {% d => { const options = { fontStyle: defaultOptional(d[0][0], 'normal'), fontVariant: defaultOptional(d[0][1], []), diff --git a/src/index.js b/src/index.js index 4434420..fd785eb 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ const transforms = [ 'flex', 'flexFlow', 'font', + 'fontFamily', 'fontVariant', 'fontWeight', 'margin', diff --git a/src/index.test.js b/src/index.test.js index c30d6ad..ba9b75b 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -13,6 +13,10 @@ it('transforms numbers', () => runTest([ ['bottom', '0'], ], { top: 0, left: 0, right: 0, bottom: 0 })); +it('transforms percentages', () => runTest([ + ['top', '50%'], +], { top: '50%' })); + it('allows decimal values', () => { expect(parseProp('number', '0.5')).toBe(0.5); expect(parseProp('number', '1.5')).toBe(1.5); @@ -24,6 +28,11 @@ it('allows decimal values', () => { expect(parseProp('number', '-100.5')).toBe(-100.5); expect(parseProp('number', '.5')).toBe(0.5); expect(parseProp('number', '-.5')).toBe(-0.5); + expect(parseProp('number', '+.5')).toBe(0.5); + expect(parseProp('number', '+5')).toBe(5); + expect(parseProp('number', '5e1')).toBe(50); + expect(parseProp('number', '5e+1')).toBe(50); + expect(parseProp('number', '5e-1')).toBe(0.5); }); it('allows decimal values in transformed values', () => runTest([ @@ -189,6 +198,18 @@ it('transforms flex shorthand with 1 values', () => runTest([ ['flex', '1'], ], { flexGrow: 1, flexShrink: 1, flexBasis: 0 })); +it('transforms flex shorthand with 3 values using percentage flex basis', () => runTest([ + ['flex', '1 2 30%'], +], { flexGrow: 1, flexShrink: 2, flexBasis: '30%' })); + +it('transforms flex shorthand with 2 values using percentage flex basis', () => runTest([ + ['flex', '1 20%'], +], { flexGrow: 1, flexShrink: 1, flexBasis: '20%' })); + +it('transforms flex shorthand with 1 value using percentage flex basis', () => runTest([ + ['flex', '10%'], +], { flexGrow: 1, flexShrink: 1, flexBasis: '10%' })); + it('transforms flexFlow shorthand with two values', () => runTest([ ['flex-flow', 'column wrap'], ], { flexDirection: 'column', flexWrap: 'wrap' })); @@ -288,6 +309,57 @@ it('omits line height if not specified', () => runTest([ fontVariant: [], })); +it('transforms font without quotes', () => runTest([ + ['font', 'bold italic small-caps 16/18 Helvetica Neue'], +], { + fontFamily: 'Helvetica Neue', + fontSize: 16, + fontWeight: 'bold', + fontStyle: 'italic', + fontVariant: ['small-caps'], + lineHeight: 18, +})); + +it('transforms font-family with double quotes', () => runTest([ + ['font-family', '"Helvetica Neue"'], +], { + fontFamily: 'Helvetica Neue', +})); + +it('transforms font-family with single quotes', () => runTest([ + ['font-family', '\'Helvetica Neue\''], +], { + fontFamily: 'Helvetica Neue', +})); + +it('transforms font-family without quotes', () => runTest([ + ['font-family', 'Helvetica Neue'], +], { + fontFamily: 'Helvetica Neue', +})); + +it('transforms font-family with quotes with otherwise invalid values', () => runTest([ + ['font-family', '"Goudy Bookletter 1911"'], +], { + fontFamily: 'Goudy Bookletter 1911', +})); + +it('transforms font-family with quotes with escaped values', () => runTest([ + ['font-family', '"test\\A test"'], +], { + fontFamily: 'test\ntest', +})); + +it('transforms font-family with quotes with escaped quote', () => runTest([ + ['font-family', '"test\\"test"'], +], { + fontFamily: 'test"test', +})); + +it('does not transform invalid unquoted font-family', () => { + expect(() => transformCss([['font-family', 'Goudy Bookletter 1911']])).toThrow(); +}); + it('allows blacklisting shorthands', () => { const actualStyles = transformCss([['border-radius', '50']], ['borderRadius']); expect(actualStyles).toEqual({ borderRadius: 50 });