From 254bd7a4b877d84109ff5d45b825945650ec6cdf Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Mon, 13 May 2024 19:30:28 +0300 Subject: [PATCH] Interprete rgb and hsl colors literally ref #316 --- documentation/dsl.md | 6 ++---- src/grammars/base.pegjs | 4 ++++ src/grammars/sonic-weave.pegjs | 13 +++++++++++++ src/interval.ts | 4 ++-- src/parser/__tests__/expression.spec.ts | 14 +++++++------- src/parser/__tests__/sonic-weave-ast.spec.ts | 19 +++++++++++++++++++ src/parser/__tests__/stdlib.spec.ts | 12 ++++++------ src/stdlib/builtin.ts | 11 ++++------- src/stdlib/public.ts | 4 ++-- 9 files changed, 59 insertions(+), 28 deletions(-) diff --git a/documentation/dsl.md b/documentation/dsl.md index e317757f..8bc0d57a 100644 --- a/documentation/dsl.md +++ b/documentation/dsl.md @@ -207,10 +207,8 @@ Colors may be specified using - [Keywords](https://www.w3.org/wiki/CSS/Properties/color/keywords) like `red`, `white` or `black` - Short hexadecimal colors like `#d13` for crimson red - Long hexadecimal colors like `#e6e6fa` for lavender -- RGB values like `rgb(160, 82, 45)` for sienna brown -- HSL values like `hsl(120, 60, 70)` for pastel green - -SonicWeave doesn't have percentages so the CSS color `hsl(120, 60%, 70%)` is spelled without the percent signs. +- RGB values like `rgb(160 82 45)` for sienna brown +- HSL values like `hsl(120deg 60% 70%)` for pastel green ## Code comments diff --git a/src/grammars/base.pegjs b/src/grammars/base.pegjs index 11c42dd5..57ab9cca 100644 --- a/src/grammars/base.pegjs +++ b/src/grammars/base.pegjs @@ -57,6 +57,10 @@ RGB4 RGB8 = $('#' HexDigit|6|) +CSSNumber + = [+-]? ('0' / ([1-9] DecimalDigit*)) ('.' DecimalDigit*)? + / [+-]? '.' DecimalDigit+ + IdentifierName = $(IdentifierStart IdentifierPart*) diff --git a/src/grammars/sonic-weave.pegjs b/src/grammars/sonic-weave.pegjs index 7381bc55..1678c6be 100644 --- a/src/grammars/sonic-weave.pegjs +++ b/src/grammars/sonic-weave.pegjs @@ -1266,6 +1266,7 @@ NotANumberLiteral InfinityLiteral = InfinityToken { return { type: 'InfinityLiteral' }; } +// RGB and HSL use modern CSS syntax, no legacy support ColorLiteral = value: (@RGB8 / @RGB4) { return { @@ -1273,6 +1274,18 @@ ColorLiteral value, }; } + / 'rgb' 'a'? '(' __ CSSNumber '%'? __ CSSNumber '%'? __ CSSNumber '%'? (__ '/' __ __ CSSNumber '%'?)? __ ')' { + return { + type: 'ColorLiteral', + value: text(), + }; + } + / 'hsl' 'a'? '(' __ CSSNumber 'deg'? __ CSSNumber '%'? __ CSSNumber '%'? (__ '/' __ CSSNumber '%'?)? __ ')' { + return { + type: 'ColorLiteral', + value: text(), + }; + } VulgarFraction 'vulgar fraction' = '¼' / 'q' / '½' / 's' / '¾' / 'Q' / [⅐-⅞] / '' diff --git a/src/interval.ts b/src/interval.ts index 5736bf28..6e96aecb 100644 --- a/src/interval.ts +++ b/src/interval.ts @@ -57,10 +57,10 @@ export class Color { /** * SonicWeave representation of the CSS color. - * @returns A string without percentage signs. + * @returns The color value as a string. */ toString() { - return this.value.replace(/%/g, ''); + return this.value; } } diff --git a/src/parser/__tests__/expression.spec.ts b/src/parser/__tests__/expression.spec.ts index 11d21237..9ccc65b5 100644 --- a/src/parser/__tests__/expression.spec.ts +++ b/src/parser/__tests__/expression.spec.ts @@ -604,16 +604,16 @@ describe('SonicWeave expression evaluator', () => { }); it('supports hsl colors', () => { - const greenish = evaluate('hsl(123, 45, 67)') as Color; - expect(greenish.value).toBe('hsl(123.000, 45.000%, 67.000%)'); - expect(greenish.toString()).toBe('hsl(123.000, 45.000, 67.000)'); + const greenish = evaluate('hsl(123deg 45% 67%)') as Color; + expect(greenish.value).toBe('hsl(123deg 45% 67%)'); + expect(greenish.toString()).toBe('hsl(123deg 45% 67%)'); const retry = evaluate(greenish.toString()) as Color; expect(retry.value).toBe(greenish.value); }); it('supports rgb color labels and reprs', () => { const lightFifth = evaluate('repr(3/2 rgb(200, 222, 256))'); - expect(lightFifth).toBe('(3/2 rgb(200.000, 222.000, 256.000))'); + expect(lightFifth).toBe('(3/2 rgb(200.000 222.000 256.000))'); }); it('can concatenate strings', () => { @@ -642,9 +642,9 @@ describe('SonicWeave expression evaluator', () => { expect(nothing).toHaveLength(0); }); - it('interpretes cents as linear decimals in rgba', () => { - const faded = evaluate('rgba(255, 255, 255, 0.5)') as Color; - expect(faded.value).toBe('rgba(255.000, 255.000, 255.000, 0.50000)'); + it('has rgba syntax', () => { + const faded = evaluate('rgba(255 255 255 / 0.5)') as Color; + expect(faded.value).toBe('rgba(255 255 255 / 0.5)'); }); it('preserves labels based on preference (left)', () => { diff --git a/src/parser/__tests__/sonic-weave-ast.spec.ts b/src/parser/__tests__/sonic-weave-ast.spec.ts index 9bf8da4c..1da07e09 100644 --- a/src/parser/__tests__/sonic-weave-ast.spec.ts +++ b/src/parser/__tests__/sonic-weave-ast.spec.ts @@ -1186,6 +1186,25 @@ describe('SonicWeave Abstract Syntax Tree parser', () => { ], }); }); + + it('parses CSS hsl literals', () => { + const ast = parseSingle('hsl( -.5deg -50% 10 / 40% )'); + expect(ast).toEqual({ + type: 'ExpressionStatement', + expression: { + type: 'ColorLiteral', + value: 'hsl( -.5deg -50% 10 / 40% )', + }, + }); + }); + + it('parses CSS rgb literals', () => { + const ast = parseSingle('rgba(255 50% 5 / .5)'); + expect(ast).toEqual({ + type: 'ExpressionStatement', + expression: {type: 'ColorLiteral', value: 'rgba(255 50% 5 / .5)'}, + }); + }); }); describe('Automatic semicolon insertion', () => { diff --git a/src/parser/__tests__/stdlib.spec.ts b/src/parser/__tests__/stdlib.spec.ts index 7467d55a..f85e9595 100644 --- a/src/parser/__tests__/stdlib.spec.ts +++ b/src/parser/__tests__/stdlib.spec.ts @@ -710,7 +710,7 @@ describe('SonicWeave standard library', () => { it('preserves labels when rotating', () => { const scale = parseSource( - '5/4 yellow "third";3/2 white "fifth";2/1 rgba(255, 255, 255, 0.5) "octave";rotate()' + '5/4 yellow "third";3/2 white "fifth";2/1 rgba(255, 255, 255, 0.5e) "octave";rotate()' ); expect(scale).toHaveLength(3); expect(scale[0].valueOf()).toBeCloseTo(6 / 5); @@ -718,7 +718,7 @@ describe('SonicWeave standard library', () => { expect(scale[0].label).toBe('fifth'); expect(scale[1].valueOf()).toBeCloseTo(8 / 5); expect(scale[1].color?.value).toBe( - 'rgba(255.000, 255.000, 255.000, 0.50000)' + 'rgba(255.000 255.000 255.000 / 0.50000)' ); expect(scale[1].label).toBe('octave'); expect(scale[2].valueOf()).toBeCloseTo(2); @@ -1101,10 +1101,10 @@ describe('SonicWeave standard library', () => { it('colors intervals based on deviation from 12ed2', () => { const scale = expand('5/4;3/2;7/4;2;edColors()'); expect(scale).toEqual([ - '5/4 hsl(310.729, 100.000, 50.000)', - '3/2 hsl(7.038, 100.000, 50.000)', - '7/4 hsl(247.773, 100.000, 50.000)', - '2 hsl(0.000, 100.000, 50.000)', + '5/4 hsl(310.729deg 100.000% 50.000%)', + '3/2 hsl(7.038deg 100.000% 50.000%)', + '7/4 hsl(247.773deg 100.000% 50.000%)', + '2 hsl(0.000deg 100.000% 50.000%)', ]); }); diff --git a/src/stdlib/builtin.ts b/src/stdlib/builtin.ts index 7480d9d8..51ad1098 100644 --- a/src/stdlib/builtin.ts +++ b/src/stdlib/builtin.ts @@ -2394,15 +2394,12 @@ help.__node__ = builtinNode(help); // CSS color generation function cc(x: Interval, fractionDigits = 3) { - if (x?.node?.type === 'CentsLiteral') { - return x.totalCents().toFixed(fractionDigits); - } return x.value.valueOf().toFixed(fractionDigits); } function rgb(red: Interval, green: Interval, blue: Interval) { requireParameters({red, green, blue}); - return new Color(`rgb(${cc(red)}, ${cc(green)}, ${cc(blue)})`); + return new Color(`rgb(${cc(red)} ${cc(green)} ${cc(blue)})`); } rgb.__doc__ = 'RGB color (Red range 0-255, Green range 0-255, Blue range 0-255).'; @@ -2411,7 +2408,7 @@ rgb.__node__ = builtinNode(rgb); function rgba(red: Interval, green: Interval, blue: Interval, alpha: Interval) { requireParameters({red, green, blue, alpha}); return new Color( - `rgba(${cc(red)}, ${cc(green)}, ${cc(blue)}, ${cc(alpha, 5)})` + `rgba(${cc(red)} ${cc(green)} ${cc(blue)} / ${cc(alpha, 5)})` ); } rgba.__doc__ = @@ -2420,7 +2417,7 @@ rgba.__node__ = builtinNode(rgba); function hsl(hue: Interval, saturation: Interval, lightness: Interval) { requireParameters({hue, saturation, lightness}); - return new Color(`hsl(${cc(hue)}, ${cc(saturation)}%, ${cc(lightness)}%)`); + return new Color(`hsl(${cc(hue)}deg ${cc(saturation)}% ${cc(lightness)}%)`); } hsl.__doc__ = 'HSL color (Hue range 0-360, Saturation range 0-100, Lightness range 0-100).'; @@ -2434,7 +2431,7 @@ function hsla( ) { requireParameters({hue, saturation, lightness, alpha}); return new Color( - `hsla(${cc(hue)}, ${cc(saturation)}%, ${cc(lightness)}%, ${cc(alpha, 5)})` + `hsla(${cc(hue)}deg ${cc(saturation)}% ${cc(lightness)}% / ${cc(alpha, 5)})` ); } hsla.__doc__ = diff --git a/src/stdlib/public.ts b/src/stdlib/public.ts index 7002f3e8..a4134a9d 100644 --- a/src/stdlib/public.ts +++ b/src/stdlib/public.ts @@ -365,7 +365,7 @@ export function centsColor(this: ExpressionVisitor, interval: Interval) { const h = octaves * 360; const s = Math.tanh(1 - octaves * 0.5) * 50 + 50; const l = Math.tanh(octaves * 0.2) * 50 + 50; - return new Color(`hsl(${h.toFixed(3)}, ${s.toFixed(3)}%, ${l.toFixed(3)}%)`); + return new Color(`hsl(${h.toFixed(3)}deg ${s.toFixed(3)}% ${l.toFixed(3)}%)`); } // Prime colors for over/under. @@ -426,7 +426,7 @@ export function factorColor(this: ExpressionVisitor, interval: Interval) { b += prgb[2] * m; } } - return new Color(`rgb(${tanh255(r)}, ${tanh255(g)}, ${tanh255(b)})`); + return new Color(`rgb(${tanh255(r)} ${tanh255(g)} ${tanh255(b)})`); } /**