Skip to content
Merged
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
98 changes: 98 additions & 0 deletions src/ExCSS.Tests/PropertyTests/CustomPropertyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Linq;
using Xunit;

namespace ExCSS.Tests.PropertyTests
{
public class CustomPropertyTests : CssConstructionFunctions
{
private static StyleDeclaration Style(string declarations)
{
var sheet = ParseStyleSheet(".a { " + declarations + " }");
return ((StyleRule)sheet.Rules[0]).Style;
}

[Theory]
// A custom property (CSS Variables 1 2) is a first-class declaration - no unknown-declaration flag
// needed - and its value is preserved verbatim.
[InlineData("--my-color: red", "--my-color", "red")]
[InlineData("--gap: 10px", "--gap", "10px")]
[InlineData("--ratio: 16 / 9", "--ratio", "16/9")]
[InlineData("--x: anything you like !", "--x", "anything you like !")]
public void CustomPropertyIsStored(string declaration, string name, string value)
{
var style = Style(declaration);

Assert.Equal(1, style.Length);
Assert.Equal(name, style[0]);
Assert.Equal(value, style[name]);
}

[Fact]
public void CustomPropertyProducesCustomPropertyType()
{
var property = ParseDeclaration("--main: blue");

Assert.IsType<CustomProperty>(property);
Assert.Equal("--main", property.Name);
Assert.True(property.HasValue);
}

[Theory]
// "--" alone (fewer than one code point after the dashes) is reserved, not a custom property.
[InlineData("-")]
[InlineData("--")]
public void NotACustomPropertyName(string name)
{
Assert.False(PropertyFactory.IsCustomPropertyName(name));
}

[Fact]
public void CustomPropertyNameCheck()
{
Assert.True(PropertyFactory.IsCustomPropertyName("--x"));
Assert.True(PropertyFactory.IsCustomPropertyName("--main-color"));
}

[Theory]
// A var() reference is accepted on any property and kept verbatim; the property's own grammar is
// not applied, since the referenced value is only known at cascade time (CSS Variables 1 3).
[InlineData("color: var(--c)", "color", "var(--c)")]
[InlineData("color: var(--c, blue)", "color", "var(--c, blue)")]
[InlineData("width: calc(var(--w) + 1px)", "width", "calc(var(--w) + 1px)")]
public void VarReferenceIsAcceptedOnLonghand(string declaration, string name, string value)
{
var style = Style(declaration);

Assert.Equal(value, style[name]);
}

[Theory]
// A shorthand whose value contains var() is kept whole - not sliced into longhands - because the
// reference can only be split after substitution (CSS Variables 1 3.2).
[InlineData("margin: var(--gap) 5px", "margin", "var(--gap) 5px")]
[InlineData("margin: var(--all)", "margin", "var(--all)")]
[InlineData("border: 1px solid var(--c)", "border", "1px solid var(--c)")]
public void VarBearingShorthandIsKeptWhole(string declaration, string name, string value)
{
var style = Style(declaration);

Assert.Equal(1, style.Length);
Assert.Equal(name, style[0]);
Assert.Equal(value, style[name]);
// The longhands are not populated at parse time.
Assert.Equal(string.Empty, style["margin-left"]);
}

[Fact]
public void CustomPropertyDashDashLexesAsOneIdent()
{
// Regression for the lexer: "--foo" must be a single ident, not '-' delimiter + "-foo".
var lexer = new Lexer(new TextSource("--foo")) { IsInValue = false };
var token = lexer.Get();

Assert.Equal(TokenType.Ident, token.Type);
Assert.Equal("--foo", token.Data);
Assert.Equal(TokenType.EndOfFile, lexer.Get().Type);
}
}
}
1 change: 1 addition & 0 deletions src/ExCSS/Enumerations/FunctionNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static class FunctionNames
public static readonly string Min = "min";
public static readonly string Max = "max";
public static readonly string Clamp = "clamp";
public static readonly string Var = "var";
public static readonly string Toggle = "toggle";
public static readonly string Translate = "translate";
public static readonly string TranslateX = "translateX";
Expand Down
16 changes: 16 additions & 0 deletions src/ExCSS/Extensions/ValueExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,22 @@ public static string ToText(this IEnumerable<Token> value)
return string.Join(string.Empty, value.Select(m => m.ToValue()));
}

// Whether the value contains a call to the named function anywhere, including nested inside another
// function's arguments - e.g. var() inside calc(var(--x) + 1px).
public static bool ContainsFunction(this IEnumerable<Token> tokens, string functionName)
{
foreach (var token in tokens)
{
if (token is FunctionToken function &&
(function.Data.Isi(functionName) || function.ArgumentTokens.ContainsFunction(functionName)))
{
return true;
}
}

return false;
}

public static Color? ToColor(this IEnumerable<Token> value)
{
var element = value.OnlyOrDefault();
Expand Down
14 changes: 13 additions & 1 deletion src/ExCSS/Factories/PropertyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,19 @@ private void AddLonghand(string name, LonghandCreator creator, bool animatable =

public Property Create(string name)
{
return CreateLonghand(name) ?? CreateShorthand(name);
return CreateLonghand(name) ?? CreateShorthand(name) ?? CreateCustomProperty(name);
}

private static Property CreateCustomProperty(string name)
{
return IsCustomPropertyName(name) ? new CustomProperty(name) : null;
}

// A custom property name is at least two dashes followed by at least one code point (CSS Custom
// Properties 1 2). "--" alone is reserved and not a valid custom property.
internal static bool IsCustomPropertyName(string name)
{
return name != null && name.Length > 2 && name[0] == '-' && name[1] == '-';
}

public Property CreateFont(string name)
Expand Down
10 changes: 10 additions & 0 deletions src/ExCSS/Model/StyleDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,16 @@ private void SetLonghand(Property property)

private void SetShorthand(ShorthandProperty shorthand)
{
if (shorthand.DeclaredValue.Original.ContainsFunction(FunctionNames.Var))
{
// A var() reference can't be split into per-longhand slices here: the referenced custom
// property's value is only known per-element, at cascade time. Keep the shorthand
// declaration whole so substitution and expansion can happen once it is resolved (CSS
// Variables 1 3.2).
SetLonghand(shorthand);
return;
}

var properties = PropertyFactory.Instance.CreateLonghandsFor(shorthand.Name);
shorthand.Export(properties);

Expand Down
15 changes: 13 additions & 2 deletions src/ExCSS/Parser/StylesheetComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -680,8 +680,19 @@ public TextPosition FillDeclarations(StyleDeclaration style)
// Example 2: "margin: 5px !important; text-align:center; margin-left: 3px;";
if (sourceProperty is ShorthandProperty shorthandProperty)
{
resolvedProperties = PropertyFactory.Instance.CreateLonghandsFor(shorthandProperty.Name);
shorthandProperty.Export(resolvedProperties);
if (shorthandProperty.DeclaredValue.Original.ContainsFunction(FunctionNames.Var))
{
// A var() reference can't be split into per-longhand slices at parse time -
// the referenced custom property's value is only known per-element, at cascade
// time. Keep the shorthand declaration whole so substitution and expansion can
// happen once it is resolved (CSS Variables 1 3.2).
resolvedProperties = new Property[] { shorthandProperty };
}
else
{
resolvedProperties = PropertyFactory.Instance.CreateLonghandsFor(shorthandProperty.Name);
shorthandProperty.Export(resolvedProperties);
}
}

foreach (var resolvedProperty in resolvedProperties)
Expand Down
16 changes: 16 additions & 0 deletions src/ExCSS/StyleProperties/CustomProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace ExCSS
{
/// <summary>
/// A custom property - a declaration whose name begins with two dashes, e.g. <c>--main-color</c>
/// (CSS Custom Properties 1 §2). Its value has no fixed grammar, so it is stored as-is.
/// </summary>
internal sealed class CustomProperty : Property
{
internal CustomProperty(string name)
: base(name)
{
}

internal override IValueConverter Converter => Converters.Any;
}
}
8 changes: 7 additions & 1 deletion src/ExCSS/StyleProperties/Property.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter)

internal bool TrySetValue(TokenValue newTokenValue)
{
var value = Converter.Convert(newTokenValue ?? TokenValue.Initial);
var tokenValue = newTokenValue ?? TokenValue.Initial;

// A value containing var() can't be validated against this property's grammar here - the
// referenced custom property is only known per-element, at cascade time - so keep it verbatim
// via Converters.Any and let substitution resolve it later (CSS Variables 1 3).
var converter = tokenValue.ContainsFunction(FunctionNames.Var) ? Converters.Any : Converter;
var value = converter.Convert(tokenValue);

if (value == null) return false;
DeclaredValue = value;
Expand Down