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
121 changes: 121 additions & 0 deletions src/ExCSS.Tests/FontPaletteTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System.Linq;
using Xunit;

namespace ExCSS.Tests
{
public class FontPaletteTests : CssConstructionFunctions
{
private string FontPalette(string value)
{
var sheet = ParseStyleSheet($".x {{ font-palette: {value}; }}");
var rule = sheet.Rules.OfType<StyleRule>().Single();
return rule.Style.GetPropertyValue("font-palette");
}

[Theory]
[InlineData("normal")]
[InlineData("light")]
[InlineData("dark")]
[InlineData("--my-palette")]
public void FontPaletteAcceptsValidValues(string value)
{
Assert.Equal(value, FontPalette(value));
}

[Theory]
[InlineData("5px")]
[InlineData("#fff")]
[InlineData("rgb(0,0,0)")]
public void FontPaletteDropsInvalidValues(string value)
{
Assert.Equal(string.Empty, FontPalette(value));
}

[Fact]
public void FontPaletteRoundTrips()
{
var sheet = ParseStyleSheet(".x { font-palette: --my-palette; }");
var rule = sheet.Rules.OfType<StyleRule>().Single();

Assert.Equal("--my-palette", rule.Style.GetPropertyValue("font-palette"));
Assert.Contains("font-palette: --my-palette", rule.ToCss());
}

[Fact]
public void FontPaletteValuesParsesNameAndDescriptors()
{
var src = "@font-palette-values --brand { font-family: \"Nabla\"; base-palette: 2; override-colors: 0 #ff0000, 1 rgb(0, 255, 0); }";
var sheet = ParseStyleSheet(src);

var rule = sheet.Rules.OfType<FontPaletteValuesRule>().Single();
Assert.Equal(RuleType.FontPaletteValues, rule.Type);
Assert.Equal("--brand", rule.Name);
Assert.Contains("Nabla", rule.Family);
Assert.Equal("2", rule.BasePalette);
Assert.Contains("#ff0000", rule.OverrideColors);
Assert.Contains("rgb(0, 255, 0)", rule.OverrideColors);
}

[Fact]
public void FontPaletteValuesLightDarkBasePalette()
{
var sheet = ParseStyleSheet("@font-palette-values --d { font-family: Nabla; base-palette: dark; }");
var rule = sheet.Rules.OfType<FontPaletteValuesRule>().Single();
Assert.Equal("dark", rule.BasePalette);
}

[Fact]
public void FontPaletteValuesRoundTrips()
{
var src = "@font-palette-values --p { font-family: Nabla; base-palette: 1; }";
var sheet = ParseStyleSheet(src);
var rule = sheet.Rules.OfType<FontPaletteValuesRule>().Single();

var css = rule.ToCss();
Assert.StartsWith("@font-palette-values --p {", css);
Assert.Contains("font-family: Nabla", css);
Assert.Contains("base-palette: 1", css);
}

[Fact]
public void FontPaletteValuesIsExposedAsIFontPaletteValuesRule()
{
var sheet = ParseStyleSheet("@font-palette-values --c { font-family: Nabla; base-palette: 1; }");
var rule = Assert.IsAssignableFrom<IFontPaletteValuesRule>(sheet.Rules.First());

Assert.Equal("--c", rule.Name);
}

[Fact]
public void FontPaletteValuesAmongOtherRules()
{
var src = ".a { color: red } @font-palette-values --p { font-family: Nabla; base-palette: 1; } .b { color: blue }";
var sheet = ParseStyleSheet(src);

Assert.Equal(3, sheet.Rules.Length);
Assert.IsType<StyleRule>(sheet.Rules[0]);
Assert.IsType<FontPaletteValuesRule>(sheet.Rules[1]);
Assert.IsType<StyleRule>(sheet.Rules[2]);
}

[Fact]
public void FontPaletteValuesDoesNotDerailFollowingRules()
{
var src = "@font-palette-values --p { font-family: Nabla; base-palette: 1; } .after { color: red; }";
var sheet = ParseStyleSheet(src);

Assert.Single(sheet.Rules.OfType<FontPaletteValuesRule>());
var styleRule = sheet.Rules.OfType<StyleRule>().Single();
Assert.Equal(".after", styleRule.SelectorText);
Assert.Equal("rgb(255, 0, 0)", styleRule.Style.GetPropertyValue("color"));
}

[Fact]
public void FontPaletteValuesNoDeclarationBlockDoesNotCrashAndFollowingRuleApplies()
{
var sheet = ParseStyleSheet("@font-palette-values --x; .after { color: red; }");
var styleRule = sheet.Rules.OfType<StyleRule>().Single();
Assert.Equal(".after", styleRule.SelectorText);
}
}
}
3 changes: 3 additions & 0 deletions src/ExCSS/Enumerations/PropertyNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ public static class PropertyNames
public static readonly string Float = "float";
public static readonly string FontFamily = "font-family";
public static readonly string FontFeatureSettings = "font-feature-settings";
public static readonly string FontPalette = "font-palette";
public static readonly string FontSize = "font-size";
public static readonly string FontSizeAdjust = "font-size-adjust";
public static readonly string FontStyle = "font-style";
Expand Down Expand Up @@ -244,6 +245,8 @@ public static class PropertyNames
public static readonly string Syntax = "syntax";
public static readonly string InitialValue = "initial-value";
public static readonly string Inherits = "inherits";
public static readonly string BasePalette = "base-palette";
public static readonly string OverrideColors = "override-colors";
public static readonly string ObjectFit = "object-fit";
// CSS Paged Media 3: the "page" property assigns a box to a named page; "size" is an @page descriptor.
public static readonly string PageName = "page";
Expand Down
1 change: 1 addition & 0 deletions src/ExCSS/Enumerations/RuleNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ public static class RuleNames
public static readonly string Page = "page";
public static readonly string Container = "container";
public static readonly string Property = "property";
public static readonly string FontPaletteValues = "font-palette-values";
}
}
3 changes: 2 additions & 1 deletion src/ExCSS/Enumerations/RuleType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public enum RuleType : byte
Viewport,
RegionStyle,
Container,
Property
Property,
FontPaletteValues
}
}
14 changes: 14 additions & 0 deletions src/ExCSS/Factories/PropertyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ private PropertyFactory()
AddLonghand(PropertyNames.FontVariant, () => new FontVariantProperty(), false, true);
AddLonghand(PropertyNames.FontWeight, () => new FontWeightProperty(), true, true);
AddLonghand(PropertyNames.FontStretch, () => new FontStretchProperty(), true, true);
AddLonghand(PropertyNames.FontPalette, () => new FontPaletteProperty());

AddShorthand(PropertyNames.Gap, () => new GapProperty(),
PropertyNames.RowGap,
Expand Down Expand Up @@ -387,6 +388,19 @@ private static bool IsPropertyDescriptor(string name)
name.Is(PropertyNames.Inherits);
}

// The @font-palette-values descriptors (font-family / base-palette / override-colors). Their values
// are stored raw via an UnknownProperty (Converters.Any); the resolver re-tokenizes them later.
public Property CreateFontPaletteDescriptor(string name)
{
return IsFontPaletteDescriptor(name) ? new UnknownProperty(name) : null;
}

private static bool IsFontPaletteDescriptor(string name)
{
return name.Is(PropertyNames.FontFamily) || name.Is(PropertyNames.BasePalette) ||
name.Is(PropertyNames.OverrideColors);
}

public Property CreateViewport(string name)
{
var feature = MediaFeatureFactory.Instance.Create(name);
Expand Down
24 changes: 24 additions & 0 deletions src/ExCSS/Parser/StylesheetComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public Rule CreateAtRule(Token token)

if (token.Data.Is(RuleNames.Property)) return CreateProperty(token);

if (token.Data.Is(RuleNames.FontPaletteValues)) return CreateFontPaletteValues(token);

return token.Data.Is(RuleNames.Document) ? CreateDocument(token) : CreateUnknown(token);
}

Expand Down Expand Up @@ -171,6 +173,28 @@ public Rule CreateProperty(Token current)
return SkipDeclarations(token);
}

public Rule CreateFontPaletteValues(Token current)
{
var rule = new FontPaletteValuesRule(_parser);
var start = current.Position;
var token = NextToken();
_nodes.Push(rule);
ParseComments(ref token);
rule.Name = GetRuleName(ref token);
ParseComments(ref token);

if (token.Type == TokenType.CurlyBracketOpen)
{
var end = FillDeclarations(rule, PropertyFactory.Instance.CreateFontPaletteDescriptor);
rule.StylesheetText = CreateView(start, end);
_nodes.Pop();
return rule;
}

_nodes.Pop();
return SkipDeclarations(token);
}

public Rule CreateImport(Token current)
{
var rule = new ImportRule(_parser);
Expand Down
50 changes: 50 additions & 0 deletions src/ExCSS/Rules/FontPaletteValuesRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.IO;
using System.Linq;

namespace ExCSS
{
/// <summary>
/// A <c>@font-palette-values</c> at-rule (CSS Fonts Module Level 4): associates a named palette override
/// (a dashed-ident such as <c>--my-palette</c>) with a font family, selecting a <c>base-palette</c> and
/// optionally overriding individual colors (<c>override-colors</c>). The descriptor block is stored like
/// <see cref="PropertyRule"/>; the name is captured from the prelude like <see cref="KeyframesRule.Name"/>.
/// </summary>
internal sealed class FontPaletteValuesRule : DeclarationRule, IFontPaletteValuesRule
{
internal FontPaletteValuesRule(StylesheetParser parser)
: base(RuleType.FontPaletteValues, RuleNames.FontPaletteValues, parser)
{
}

protected override Property CreateNewProperty(string name)
{
return PropertyFactory.Instance.CreateFontPaletteDescriptor(name);
}

public string Name { get; set; }

public string Family
{
get => GetValue(PropertyNames.FontFamily);
set => SetValue(PropertyNames.FontFamily, value);
}

public string BasePalette
{
get => GetValue(PropertyNames.BasePalette);
set => SetValue(PropertyNames.BasePalette, value);
}

public string OverrideColors
{
get => GetValue(PropertyNames.OverrideColors);
set => SetValue(PropertyNames.OverrideColors, value);
}

public override void ToCss(TextWriter writer, IStyleFormatter formatter)
{
var declarations = formatter.Declarations(Declarations.Where(d => d.HasValue).Select(d => d.ToCss(formatter)));
writer.Write(string.Concat("@font-palette-values ", Name, " { ", declarations, " }"));
}
}
}
21 changes: 21 additions & 0 deletions src/ExCSS/Rules/IFontPaletteValuesRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace ExCSS
{
/// <summary>
/// A palette override declared with a <c>@font-palette-values</c> at-rule (CSS Fonts Module Level 4).
/// Exposes the prelude name and the three descriptors.
/// </summary>
public interface IFontPaletteValuesRule : IRule, IProperties
{
/// <summary>The palette name, a dashed-ident referenced by <c>font-palette: &lt;dashed-ident&gt;</c>, e.g. <c>--my-palette</c>.</summary>
string Name { get; set; }

/// <summary>The raw <c>font-family</c> descriptor value (the family this palette applies to).</summary>
string Family { get; set; }

/// <summary>The raw <c>base-palette</c> descriptor value (<c>&lt;integer&gt; | light | dark</c>).</summary>
string BasePalette { get; set; }

/// <summary>The raw <c>override-colors</c> descriptor value (a comma list of <c>&lt;index&gt; &lt;color&gt;</c> pairs).</summary>
string OverrideColors { get; set; }
}
}
19 changes: 19 additions & 0 deletions src/ExCSS/StyleProperties/Font/FontPaletteProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace ExCSS
{
/// <summary>
/// The <c>font-palette</c> property (CSS Fonts Module Level 4): selects a color palette for a COLR/CPAL
/// color font. Inherited. The value grammar (<c>normal | light | dark | &lt;dashed-ident&gt;</c>) is
/// validated by <see cref="FontPaletteValueConverter"/>; the authored text is preserved verbatim.
/// </summary>
internal sealed class FontPaletteProperty : Property
{
private static readonly IValueConverter StyleConverter = new FontPaletteValueConverter().OrDefault();

internal FontPaletteProperty()
: base(PropertyNames.FontPalette, PropertyFlags.Inherited)
{
}

internal override IValueConverter Converter => StyleConverter;
}
}
51 changes: 51 additions & 0 deletions src/ExCSS/ValueConverters/FontPaletteValueConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace ExCSS
{
/// <summary>
/// Validates a <c>font-palette</c> value (CSS Fonts Module Level 4): the keywords <c>normal</c>/
/// <c>light</c>/<c>dark</c>, or a <c>&lt;dashed-ident&gt;</c> naming an <c>@font-palette-values</c> rule.
/// Anything else is dropped at parse time. The authored value text is preserved verbatim, mirroring
/// <see cref="ClipPathValueConverter"/>. (The <c>palette-mix()</c> form of the grammar is not supported.)
/// </summary>
internal sealed class FontPaletteValueConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable<Token> value)
{
var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray();

if (tokens.Length == 1 && tokens[0].Type == TokenType.Ident &&
IsKeywordOrDashedIdent(tokens[0].Data))
{
return new FontPaletteValue(value);
}

return null;
}

public IPropertyValue Construct(Property[] properties)
{
return properties.Guard<FontPaletteValue>();
}

private static bool IsKeywordOrDashedIdent(string ident) =>
ident.Isi(Keywords.Normal) || ident.Isi("light") || ident.Isi("dark") ||
ident.StartsWith("--", StringComparison.Ordinal);

private sealed class FontPaletteValue : IPropertyValue
{
public FontPaletteValue(IEnumerable<Token> tokens)
{
Original = new TokenValue(tokens);
}

public string CssText => Original.Text;

public TokenValue Original { get; }

public TokenValue ExtractFor(string name) => Original;
}
}
}