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

namespace ExCSS.Tests.PropertyTests
{
public class PageRuleTests : CssConstructionFunctions
{
private static PageRule ParsePage(string source)
{
var sheet = ParseStyleSheet(source);
return (PageRule)sheet.Rules.First();
}

[Theory]
// CSS Paged Media 3 4.1: an @page selector is a comma-separated list of "[<name>]? [:<pseudo>]?"
// entries.
[InlineData("@page :left { margin: 1cm }", ":left")]
[InlineData("@page :first { margin: 1cm }", ":first")]
[InlineData("@page chapter { margin: 1cm }", "chapter")]
[InlineData("@page chapter:left { margin: 1cm }", "chapter:left")]
[InlineData("@page chapter1:left, chapter2:left { margin: 1cm }", "chapter1:left, chapter2:left")]
[InlineData("@page a, b, c { margin: 1cm }", "a, b, c")]
public void PageSelectorForms(string source, string expectedSelector)
{
var rule = ParsePage(source);

Assert.Equal(RuleType.Page, rule.Type);
Assert.Equal(expectedSelector, rule.SelectorText);
}

[Fact]
public void PageSelectorListEntriesAreExposed()
{
var rule = ParsePage("@page chapter1:left, chapter2:right { margin: 1cm }");
var selector = Assert.IsType<PageSelector>(rule.Selector);

Assert.Equal(2, selector.Entries.Count);
Assert.Equal("chapter1", selector.Entries[0].Name);
Assert.Equal("left", selector.Entries[0].Pseudo);
Assert.Equal("chapter2", selector.Entries[1].Name);
Assert.Equal("right", selector.Entries[1].Pseudo);
}

[Theory]
// The @page "size" descriptor (CSS Paged Media 3 6.3).
[InlineData("size: A4", "A4")]
[InlineData("size: letter", "letter")]
[InlineData("size: landscape", "landscape")]
[InlineData("size: 8.5in 11in", "8.5in 11in")]
[InlineData("size: 210mm", "210mm")]
public void PageSizeDescriptor(string declaration, string value)
{
var style = ParseDeclarations(declaration);

Assert.Equal(value, style.Size);
}

[Theory]
// The "page" property (CSS Paged Media 3 3.4).
[InlineData("page: chapter", "chapter")]
[InlineData("page: auto", "auto")]
public void PageProperty(string declaration, string value)
{
var style = ParseDeclarations(declaration);

Assert.Equal(value, style.PageName);
}
}
}
3 changes: 3 additions & 0 deletions src/ExCSS/Enumerations/PropertyNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ public static class PropertyNames
public static readonly string UnicodeRange = "unicode-range";
public static readonly string Src = "src";
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";
public static readonly string Size = "size";
public static readonly string ObjectPosition = "object-position";
}
}
2 changes: 2 additions & 0 deletions src/ExCSS/Factories/PropertyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ private PropertyFactory()
AddLonghand(PropertyNames.WordWrap, () => new OverflowWrapProperty());
AddLonghand(PropertyNames.ZIndex, () => new ZIndexProperty(), true);
AddLonghand(PropertyNames.ObjectFit, () => new ObjectFitProperty());
AddLonghand(PropertyNames.PageName, () => new PageNameProperty());
AddLonghand(PropertyNames.Size, () => new PageSizeProperty());
AddLonghand(PropertyNames.ObjectPosition, () => new ObjectPositionProperty(), true);

_fonts.Add(PropertyNames.Src, () => new SrcProperty());
Expand Down
12 changes: 12 additions & 0 deletions src/ExCSS/Model/StyleDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1681,5 +1681,17 @@ public string Zoom
get => GetPropertyValue(PropertyNames.Zoom);
set => SetPropertyValue(PropertyNames.Zoom, value);
}

public string Size
{
get => GetPropertyValue(PropertyNames.Size);
set => SetPropertyValue(PropertyNames.Size, value);
}

public string PageName
{
get => GetPropertyValue(PropertyNames.PageName);
set => SetPropertyValue(PropertyNames.PageName, value);
}
}
}
47 changes: 36 additions & 11 deletions src/ExCSS/Parser/StylesheetComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -526,21 +526,46 @@ public KeyframeSelector CreateKeyframeSelector(ref Token token)

private PageSelector CreatePageSelector(ref Token token)
{
PageSelector selector;

if (token.Type == TokenType.Colon)
// The CSS Paged Media 3 selector grammar, per comma-separated entry: an optional <ident> page
// name followed by an optional :<ident> pseudo-class - "@page chapter1:left, chapter2:left"
// (name+pseudo), "@page chapter" (name only), "@page :first" (pseudo only). Page names are
// case-sensitive custom-idents; pseudo-class keywords (first/left/right) are matched
// case-insensitively by the caller.
var entries = new List<PageSelectorEntry>();

while (true)
{
// Add the pseudo class selector
token = NextToken();
selector = token.Type == TokenType.Ident ? new PageSelector(token.Data) : new PageSelector();
string name = null;
string pseudo = null;

if (token.Type == TokenType.Ident)
{
name = token.Data;
token = NextToken();
}

if (token.Type == TokenType.Colon)
{
token = NextToken();
if (token.Type == TokenType.Ident)
{
pseudo = token.Data;
token = NextToken();
}
}

if (name != null || pseudo != null)
entries.Add(new PageSelectorEntry(name, pseudo));

ParseComments(ref token);

if (token.Type != TokenType.Comma) break;

//Skip past the pseudo class identifier
token = NextToken();
ParseComments(ref token);
}
else
{
selector = new PageSelector();
}

var selector = new PageSelector(entries);

//var start = token.Position;

Expand Down
38 changes: 30 additions & 8 deletions src/ExCSS/Selectors/KeyframeSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,45 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter)
public string Text => this.ToCss();
}

public sealed class PageSelector : StylesheetNode, ISelector
/// <summary>
/// One comma-separated entry of an <c>@page</c> selector list, e.g. the two entries of
/// <c>@page chapter1:left, chapter2:left</c> are <c>("chapter1", "left")</c> and
/// <c>("chapter2", "left")</c>. Either part may be absent: <c>@page chapter</c> has a null pseudo;
/// <c>@page :first</c> has a null name.
/// </summary>
public readonly struct PageSelectorEntry
{
private readonly string _name;

public PageSelector(string name)
public PageSelectorEntry(string name, string pseudo)
{
_name = name;
Name = name;
Pseudo = pseudo;
}

public PageSelector() : this(string.Empty)
public string Name { get; }
public string Pseudo { get; }
}

public sealed class PageSelector : StylesheetNode, ISelector
{
private readonly List<PageSelectorEntry> _entries;

public PageSelector(IEnumerable<PageSelectorEntry> entries)
{
_entries = new List<PageSelectorEntry>(entries);
}

public IReadOnlyList<PageSelectorEntry> Entries => _entries;

public override void ToCss(TextWriter writer, IStyleFormatter formatter)
{
var pseudo = _name == string.Empty ? "" : ":";
writer.Write($"{pseudo}{_name}");
for (var i = 0; i < _entries.Count; i++)
{
if (i > 0) writer.Write(", ");

var entry = _entries[i];
if (entry.Name != null) writer.Write(entry.Name);
if (entry.Pseudo != null) writer.Write($":{entry.Pseudo}");
}
}

public Priority Specificity => Priority.Inline;
Expand Down
18 changes: 18 additions & 0 deletions src/ExCSS/StyleProperties/PageNameProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace ExCSS
{
using static Converters;

/// <summary>
/// The <c>page</c> property (CSS Paged Media 3 §3.4): assigns a box to a named page defined by an
/// <c>@page &lt;name&gt;</c> rule.
/// </summary>
internal sealed class PageNameProperty : Property
{
internal PageNameProperty()
: base(PropertyNames.PageName)
{
}

internal override IValueConverter Converter => IdentifierConverter.OrDefault();
}
}
20 changes: 20 additions & 0 deletions src/ExCSS/StyleProperties/PageSizeProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace ExCSS
{
using static Converters;

/// <summary>
/// The <c>size</c> descriptor of an <c>@page</c> rule (CSS Paged Media 3 §6.3): a page-size keyword
/// (<c>A4</c>, <c>letter</c>, …), an orientation (<c>portrait</c>/<c>landscape</c>), or one or two
/// explicit lengths.
/// </summary>
internal sealed class PageSizeProperty : Property
{
internal PageSizeProperty()
: base(PropertyNames.Size)
{
}

internal override IValueConverter Converter =>
IdentifierConverter.Or(LengthConverter).Many(1, 2).OrDefault();
}
}