Skip to content

Commit df01452

Browse files
authored
Merge pull request #214 from jhaygood86/feature/paged-media
Support @page named selectors, selector lists, size, and the page property
2 parents 0e7eca0 + dde239e commit df01452

8 files changed

Lines changed: 190 additions & 19 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Linq;
2+
using Xunit;
3+
4+
namespace ExCSS.Tests.PropertyTests
5+
{
6+
public class PageRuleTests : CssConstructionFunctions
7+
{
8+
private static PageRule ParsePage(string source)
9+
{
10+
var sheet = ParseStyleSheet(source);
11+
return (PageRule)sheet.Rules.First();
12+
}
13+
14+
[Theory]
15+
// CSS Paged Media 3 4.1: an @page selector is a comma-separated list of "[<name>]? [:<pseudo>]?"
16+
// entries.
17+
[InlineData("@page :left { margin: 1cm }", ":left")]
18+
[InlineData("@page :first { margin: 1cm }", ":first")]
19+
[InlineData("@page chapter { margin: 1cm }", "chapter")]
20+
[InlineData("@page chapter:left { margin: 1cm }", "chapter:left")]
21+
[InlineData("@page chapter1:left, chapter2:left { margin: 1cm }", "chapter1:left, chapter2:left")]
22+
[InlineData("@page a, b, c { margin: 1cm }", "a, b, c")]
23+
public void PageSelectorForms(string source, string expectedSelector)
24+
{
25+
var rule = ParsePage(source);
26+
27+
Assert.Equal(RuleType.Page, rule.Type);
28+
Assert.Equal(expectedSelector, rule.SelectorText);
29+
}
30+
31+
[Fact]
32+
public void PageSelectorListEntriesAreExposed()
33+
{
34+
var rule = ParsePage("@page chapter1:left, chapter2:right { margin: 1cm }");
35+
var selector = Assert.IsType<PageSelector>(rule.Selector);
36+
37+
Assert.Equal(2, selector.Entries.Count);
38+
Assert.Equal("chapter1", selector.Entries[0].Name);
39+
Assert.Equal("left", selector.Entries[0].Pseudo);
40+
Assert.Equal("chapter2", selector.Entries[1].Name);
41+
Assert.Equal("right", selector.Entries[1].Pseudo);
42+
}
43+
44+
[Theory]
45+
// The @page "size" descriptor (CSS Paged Media 3 6.3).
46+
[InlineData("size: A4", "A4")]
47+
[InlineData("size: letter", "letter")]
48+
[InlineData("size: landscape", "landscape")]
49+
[InlineData("size: 8.5in 11in", "8.5in 11in")]
50+
[InlineData("size: 210mm", "210mm")]
51+
public void PageSizeDescriptor(string declaration, string value)
52+
{
53+
var style = ParseDeclarations(declaration);
54+
55+
Assert.Equal(value, style.Size);
56+
}
57+
58+
[Theory]
59+
// The "page" property (CSS Paged Media 3 3.4).
60+
[InlineData("page: chapter", "chapter")]
61+
[InlineData("page: auto", "auto")]
62+
public void PageProperty(string declaration, string value)
63+
{
64+
var style = ParseDeclarations(declaration);
65+
66+
Assert.Equal(value, style.PageName);
67+
}
68+
}
69+
}

src/ExCSS/Enumerations/PropertyNames.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ public static class PropertyNames
239239
public static readonly string UnicodeRange = "unicode-range";
240240
public static readonly string Src = "src";
241241
public static readonly string ObjectFit = "object-fit";
242+
// CSS Paged Media 3: the "page" property assigns a box to a named page; "size" is an @page descriptor.
243+
public static readonly string PageName = "page";
244+
public static readonly string Size = "size";
242245
public static readonly string ObjectPosition = "object-position";
243246
}
244247
}

src/ExCSS/Factories/PropertyFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ private PropertyFactory()
335335
AddLonghand(PropertyNames.WordWrap, () => new OverflowWrapProperty());
336336
AddLonghand(PropertyNames.ZIndex, () => new ZIndexProperty(), true);
337337
AddLonghand(PropertyNames.ObjectFit, () => new ObjectFitProperty());
338+
AddLonghand(PropertyNames.PageName, () => new PageNameProperty());
339+
AddLonghand(PropertyNames.Size, () => new PageSizeProperty());
338340
AddLonghand(PropertyNames.ObjectPosition, () => new ObjectPositionProperty(), true);
339341

340342
_fonts.Add(PropertyNames.Src, () => new SrcProperty());

src/ExCSS/Model/StyleDeclaration.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,5 +1681,17 @@ public string Zoom
16811681
get => GetPropertyValue(PropertyNames.Zoom);
16821682
set => SetPropertyValue(PropertyNames.Zoom, value);
16831683
}
1684+
1685+
public string Size
1686+
{
1687+
get => GetPropertyValue(PropertyNames.Size);
1688+
set => SetPropertyValue(PropertyNames.Size, value);
1689+
}
1690+
1691+
public string PageName
1692+
{
1693+
get => GetPropertyValue(PropertyNames.PageName);
1694+
set => SetPropertyValue(PropertyNames.PageName, value);
1695+
}
16841696
}
16851697
}

src/ExCSS/Parser/StylesheetComposer.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -526,21 +526,46 @@ public KeyframeSelector CreateKeyframeSelector(ref Token token)
526526

527527
private PageSelector CreatePageSelector(ref Token token)
528528
{
529-
PageSelector selector;
530-
531-
if (token.Type == TokenType.Colon)
529+
// The CSS Paged Media 3 selector grammar, per comma-separated entry: an optional <ident> page
530+
// name followed by an optional :<ident> pseudo-class - "@page chapter1:left, chapter2:left"
531+
// (name+pseudo), "@page chapter" (name only), "@page :first" (pseudo only). Page names are
532+
// case-sensitive custom-idents; pseudo-class keywords (first/left/right) are matched
533+
// case-insensitively by the caller.
534+
var entries = new List<PageSelectorEntry>();
535+
536+
while (true)
532537
{
533-
// Add the pseudo class selector
534-
token = NextToken();
535-
selector = token.Type == TokenType.Ident ? new PageSelector(token.Data) : new PageSelector();
538+
string name = null;
539+
string pseudo = null;
540+
541+
if (token.Type == TokenType.Ident)
542+
{
543+
name = token.Data;
544+
token = NextToken();
545+
}
546+
547+
if (token.Type == TokenType.Colon)
548+
{
549+
token = NextToken();
550+
if (token.Type == TokenType.Ident)
551+
{
552+
pseudo = token.Data;
553+
token = NextToken();
554+
}
555+
}
556+
557+
if (name != null || pseudo != null)
558+
entries.Add(new PageSelectorEntry(name, pseudo));
559+
560+
ParseComments(ref token);
561+
562+
if (token.Type != TokenType.Comma) break;
536563

537-
//Skip past the pseudo class identifier
538564
token = NextToken();
565+
ParseComments(ref token);
539566
}
540-
else
541-
{
542-
selector = new PageSelector();
543-
}
567+
568+
var selector = new PageSelector(entries);
544569

545570
//var start = token.Position;
546571

src/ExCSS/Selectors/KeyframeSelector.cs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,45 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter)
2828
public string Text => this.ToCss();
2929
}
3030

31-
public sealed class PageSelector : StylesheetNode, ISelector
31+
/// <summary>
32+
/// One comma-separated entry of an <c>@page</c> selector list, e.g. the two entries of
33+
/// <c>@page chapter1:left, chapter2:left</c> are <c>("chapter1", "left")</c> and
34+
/// <c>("chapter2", "left")</c>. Either part may be absent: <c>@page chapter</c> has a null pseudo;
35+
/// <c>@page :first</c> has a null name.
36+
/// </summary>
37+
public readonly struct PageSelectorEntry
3238
{
33-
private readonly string _name;
34-
35-
public PageSelector(string name)
39+
public PageSelectorEntry(string name, string pseudo)
3640
{
37-
_name = name;
41+
Name = name;
42+
Pseudo = pseudo;
3843
}
3944

40-
public PageSelector() : this(string.Empty)
45+
public string Name { get; }
46+
public string Pseudo { get; }
47+
}
48+
49+
public sealed class PageSelector : StylesheetNode, ISelector
50+
{
51+
private readonly List<PageSelectorEntry> _entries;
52+
53+
public PageSelector(IEnumerable<PageSelectorEntry> entries)
4154
{
55+
_entries = new List<PageSelectorEntry>(entries);
4256
}
4357

58+
public IReadOnlyList<PageSelectorEntry> Entries => _entries;
59+
4460
public override void ToCss(TextWriter writer, IStyleFormatter formatter)
4561
{
46-
var pseudo = _name == string.Empty ? "" : ":";
47-
writer.Write($"{pseudo}{_name}");
62+
for (var i = 0; i < _entries.Count; i++)
63+
{
64+
if (i > 0) writer.Write(", ");
65+
66+
var entry = _entries[i];
67+
if (entry.Name != null) writer.Write(entry.Name);
68+
if (entry.Pseudo != null) writer.Write($":{entry.Pseudo}");
69+
}
4870
}
4971

5072
public Priority Specificity => Priority.Inline;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace ExCSS
2+
{
3+
using static Converters;
4+
5+
/// <summary>
6+
/// The <c>page</c> property (CSS Paged Media 3 §3.4): assigns a box to a named page defined by an
7+
/// <c>@page &lt;name&gt;</c> rule.
8+
/// </summary>
9+
internal sealed class PageNameProperty : Property
10+
{
11+
internal PageNameProperty()
12+
: base(PropertyNames.PageName)
13+
{
14+
}
15+
16+
internal override IValueConverter Converter => IdentifierConverter.OrDefault();
17+
}
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace ExCSS
2+
{
3+
using static Converters;
4+
5+
/// <summary>
6+
/// The <c>size</c> descriptor of an <c>@page</c> rule (CSS Paged Media 3 §6.3): a page-size keyword
7+
/// (<c>A4</c>, <c>letter</c>, …), an orientation (<c>portrait</c>/<c>landscape</c>), or one or two
8+
/// explicit lengths.
9+
/// </summary>
10+
internal sealed class PageSizeProperty : Property
11+
{
12+
internal PageSizeProperty()
13+
: base(PropertyNames.Size)
14+
{
15+
}
16+
17+
internal override IValueConverter Converter =>
18+
IdentifierConverter.Or(LengthConverter).Many(1, 2).OrDefault();
19+
}
20+
}

0 commit comments

Comments
 (0)