Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions src/ExCSS.Tests/PropertyTests/TransformProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,36 @@ public void CssTransformStyleNoneIllegal()
Assert.False(concrete.HasValue);
}

[Fact]
public void CssTransformOriginVerticalKeywordThenLengthIllegal()
{
// "top 2px" is invalid (CSS Transforms 1): the trailing <length> z-offset only exists in the
// two-value production, but "top" is a vertical-only keyword that cannot fill the horizontal
// position, and there is no second position value - so this is neither a valid single value nor a
// valid two-value pair, and the "2px" cannot be reinterpreted as a z-offset.
var snippet = "transform-origin: top 2px ";
var property = ParseDeclaration(snippet);
Assert.Equal("transform-origin", property.Name);
Assert.False(property.IsImportant);
Assert.IsType<TransformOriginProperty>(property);
var concrete = (TransformOriginProperty)property;
Assert.False(concrete.HasValue);
}

[Fact]
public void CssTransformOriginVerticalKeywordThenLengthWithZIllegal()
{
// As above, invalid for the same reason; a trailing <length> z-offset cannot rescue an invalid
// single-keyword-plus-length position.
var snippet = "transform-origin: top 2px 10px ";
var property = ParseDeclaration(snippet);
Assert.Equal("transform-origin", property.Name);
Assert.False(property.IsImportant);
Assert.IsType<TransformOriginProperty>(property);
var concrete = (TransformOriginProperty)property;
Assert.False(concrete.HasValue);
}

[Fact]
public void CssTransformOriginXOffsetLegal()
{
Expand Down Expand Up @@ -257,17 +287,19 @@ public void CssTransformOriginYOffsetLegal()
}

[Fact]
public void CssTransformOriginYOffsetXKeywordLegal()
public void CssTransformOriginLengthThenWrongAxisKeywordIllegal()
{
// "2px left" is invalid (CSS Transforms 1): a length in the first (horizontal) position forces
// the ordered two-value form, whose second value must be a vertical keyword or a <length-percentage>
// - but "left" is a horizontal-only keyword, so the pair is invalid. Only the keyword-only form may
// be reordered.
var snippet = "transform-origin: 2px left";
var property = ParseDeclaration(snippet);
Assert.Equal("transform-origin", property.Name);
Assert.False(property.IsImportant);
Assert.IsType<TransformOriginProperty>(property);
var concrete = (TransformOriginProperty)property;
Assert.False(concrete.IsInherited);
Assert.True(concrete.HasValue);
Assert.Equal("2px left", concrete.Value);
Assert.False(concrete.HasValue);
}

[Fact]
Expand Down Expand Up @@ -327,17 +359,17 @@ public void CssTransformOriginXYZLegal()
}

[Fact]
public void CssTransformOriginYXKeywordZLegal()
public void CssTransformOriginLengthThenWrongAxisKeywordWithZIllegal()
{
// As above, invalid for the same reason; the trailing <length> z-offset cannot rescue an
// invalid horizontal/vertical pair.
var snippet = "transform-origin: 2px left 10px ";
var property = ParseDeclaration(snippet);
Assert.Equal("transform-origin", property.Name);
Assert.False(property.IsImportant);
Assert.IsType<TransformOriginProperty>(property);
var concrete = (TransformOriginProperty)property;
Assert.False(concrete.IsInherited);
Assert.True(concrete.HasValue);
Assert.Equal("2px left 10px", concrete.Value);
Assert.False(concrete.HasValue);
}

[Fact]
Expand Down
78 changes: 56 additions & 22 deletions src/ExCSS/StyleProperties/Transform/TransformOriginProperty.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,63 @@
namespace ExCSS
namespace ExCSS
{
using static Converters;

internal sealed class TransformOriginProperty : Property
{
private static readonly IValueConverter StyleConverter = WithOrder(
LengthOrPercentConverter.Or(Keywords.Center, Point.Center).Or(WithAny(
LengthOrPercentConverter.Or(Keywords.Left, Length.Zero)
.Or(Keywords.Right, Length.Full)
.Or(Keywords.Center, Length.Half)
.Option(Length.Half),
LengthOrPercentConverter.Or(Keywords.Top, Length.Zero)
.Or(Keywords.Bottom, Length.Full)
.Or(Keywords.Center, Length.Half)
.Option(Length.Half))).Or(
WithAny(
LengthOrPercentConverter.Or(Keywords.Top, Length.Zero)
.Or(Keywords.Bottom, Length.Full)
.Or(Keywords.Center, Length.Half)
.Option(Length.Half),
LengthOrPercentConverter.Or(Keywords.Left, Length.Zero)
.Or(Keywords.Right, Length.Full)
.Or(Keywords.Center, Length.Half)
.Option(Length.Half))).Required(),
LengthConverter.Option(Length.Zero)).OrDefault(Point.Center);
// CSS Transforms 1 (https://www.w3.org/TR/css-transforms-1/#transform-origin-property):
// [ left | center | right | top | bottom | <length-percentage> ]
// | [ left | center | right | <length-percentage> ] [ top | center | bottom | <length-percentage> ] <length>?
// | [ [ center | left | right ] && [ center | top | bottom ] ] <length>?
//
// The two-value <length-percentage> form is ORDERED (horizontal then vertical). Only the keyword-only
// form may be reordered ("center left" == "left center"). So "2px left" and "top 2px" are invalid: a
// length in one axis forces the ordered form, and the remaining token is a keyword for the wrong axis.
private static readonly IValueConverter Horizontal =
LengthOrPercentConverter
.Or(Keywords.Left, Length.Zero)
.Or(Keywords.Right, Length.Full)
.Or(Keywords.Center, Length.Half);

private static readonly IValueConverter Vertical =
LengthOrPercentConverter
.Or(Keywords.Top, Length.Zero)
.Or(Keywords.Bottom, Length.Full)
.Or(Keywords.Center, Length.Half);

private static readonly IValueConverter HorizontalKeyword =
Assign(Keywords.Left, Length.Zero)
.Or(Keywords.Right, Length.Full)
.Or(Keywords.Center, Length.Half);

private static readonly IValueConverter VerticalKeyword =
Assign(Keywords.Top, Length.Zero)
.Or(Keywords.Bottom, Length.Full)
.Or(Keywords.Center, Length.Half);

// A one- or two-value <position> with no z-offset (this is exactly the perspective-origin grammar).
private static readonly IValueConverter Position =
LengthOrPercentConverter.Or(Keywords.Center, Point.Center)
// Ordered horizontal-then-vertical (rejects a keyword in the wrong axis position).
.Or(WithOrder(Horizontal.Option(Length.Half), Vertical.Option(Length.Half)))
// Keyword-only form, any order ("center left"): a token accepted by both axes must not be
// claimed positionally, so this needs order-independent matching.
.Or(WithAnyOrderIndependent(HorizontalKeyword.Option(Length.Half), VerticalKeyword.Option(Length.Half)));

// A genuine two-value position: BOTH a horizontal and a vertical component must be present. The axis
// converters are bare (not .Option), so each must actually consume a token. The <length> z-offset only
// exists in this two-value production, so gating the z-offset behind this (rather than the shared outer
// WithOrder) is what makes a single-value position + length such as "top 2px" invalid.
private static readonly IValueConverter TwoValuePosition =
WithOrder(Horizontal, Vertical)
.Or(WithAnyOrderIndependent(HorizontalKeyword, VerticalKeyword));

private static readonly IValueConverter StyleConverter =
// Two-value position followed by a (required) <length> z-offset - this branch only matches when a
// third length token is actually present...
WithOrder(TwoValuePosition, LengthConverter)
// ...otherwise a plain one- or two-value <position> with no z-offset.
.Or(Position)
.OrDefault(Point.Center);

internal TransformOriginProperty()
: base(PropertyNames.TransformOrigin, PropertyFlags.Animatable)
Expand All @@ -32,4 +66,4 @@ internal TransformOriginProperty()

internal override IValueConverter Converter => StyleConverter;
}
}
}