blob: e54a1c79a516a0bdb916188414cb73670ed6b39a [file] [log] [blame]
package parser
import (
"fmt"
"strings"
"testing"
"github.com/chris-ramon/douceur/css"
)
func MustParse(t *testing.T, txt string, nbRules int) *css.Stylesheet {
stylesheet, err := Parse(txt)
if err != nil {
t.Fatal("Failed to parse CSS", err, txt)
}
if len(stylesheet.Rules) != nbRules {
t.Fatal(fmt.Sprintf("Failed to parse CSS \"%s\", expected %d rules but got %d", txt, nbRules, len(stylesheet.Rules)))
}
return stylesheet
}
func MustEqualRule(t *testing.T, parsedRule *css.Rule, expectedRule *css.Rule) {
if !parsedRule.Equal(expectedRule) {
diff := parsedRule.Diff(expectedRule)
t.Fatal(fmt.Sprintf("Rule parsing error\nExpected:\n\"%s\"\nGot:\n\"%s\"\nDiff:\n%s", expectedRule, parsedRule, strings.Join(diff, "\n")))
}
}
func MustEqualCSS(t *testing.T, ruleString string, expected string) {
if ruleString != expected {
t.Fatal(fmt.Sprintf("CSS generation error\n Expected:\n\"%s\"\n Got:\n\"%s\"", expected, ruleString))
}
}
func TestQualifiedRule(t *testing.T) {
input := `/* This is a comment */
p > a {
color: blue;
text-decoration: underline; /* This is a comment */
}`
expectedRule := &css.Rule{
Kind: css.QualifiedRule,
Prelude: "p > a",
Selectors: []*css.Selector{
{
Value: "p > a",
Line: 2,
Column: 1,
},
},
Declarations: []*css.Declaration{
{
Property: "color",
Value: "blue",
Line: 3,
Column: 5,
},
{
Property: "text-decoration",
Value: "underline",
Line: 4,
Column: 5,
},
},
}
expectedOutput := `p > a {
color: blue;
text-decoration: underline;
}`
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), expectedOutput)
}
func TestQualifiedRuleImportant(t *testing.T) {
input := `/* This is a comment */
p > a {
color: blue;
text-decoration: underline !important;
font-weight: normal !IMPORTANT ;
}`
expectedRule := &css.Rule{
Kind: css.QualifiedRule,
Prelude: "p > a",
Selectors: []*css.Selector{
{
Value: "p > a",
Line: 2,
Column: 1,
},
},
Declarations: []*css.Declaration{
{
Property: "color",
Value: "blue",
Important: false,
Line: 3,
Column: 5,
},
{
Property: "text-decoration",
Value: "underline",
Important: true,
Line: 4,
Column: 5,
},
{
Property: "font-weight",
Value: "normal",
Important: true,
Line: 5,
Column: 5,
},
},
}
expectedOutput := `p > a {
color: blue;
text-decoration: underline !important;
font-weight: normal !important;
}`
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), expectedOutput)
}
func TestQualifiedRuleSelectors(t *testing.T) {
input := `table, tr, td {
padding: 0;
}
body,
h1, h2,
h3 {
color: #fff;
}`
expectedRule1 := &css.Rule{
Kind: css.QualifiedRule,
Prelude: "table, tr, td",
Selectors: []*css.Selector{
{
Value: "table",
Line: 1,
Column: 1,
},
{
Value: "tr",
Line: 1,
Column: 8,
},
{
Value: "td",
Line: 1,
Column: 12,
},
},
Declarations: []*css.Declaration{
{
Property: "padding",
Value: "0",
Line: 2,
Column: 3,
},
},
}
expectedRule2 := &css.Rule{
Kind: css.QualifiedRule,
Prelude: `body,
h1, h2,
h3`,
Selectors: []*css.Selector{
{
Value: "body",
Line: 5,
Column: 1,
},
{
Value: "h1",
Line: 6,
Column: 3,
},
{
Value: "h2",
Line: 6,
Column: 9,
},
{
Value: "h3",
Line: 7,
Column: 5,
},
},
Declarations: []*css.Declaration{
{
Property: "color",
Value: "#fff",
Line: 8,
Column: 3,
},
},
}
expectedOutput := `table, tr, td {
padding: 0;
}
body, h1, h2, h3 {
color: #fff;
}`
stylesheet := MustParse(t, input, 2)
MustEqualRule(t, stylesheet.Rules[0], expectedRule1)
MustEqualRule(t, stylesheet.Rules[1], expectedRule2)
MustEqualCSS(t, stylesheet.String(), expectedOutput)
}
func TestAtRuleCharset(t *testing.T) {
input := `@charset "UTF-8";`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@charset",
Prelude: "\"UTF-8\"",
}
expectedOutput := `@charset "UTF-8";`
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), expectedOutput)
}
func TestAtRuleCounterStyle(t *testing.T) {
input := `@counter-style footnote {
system: symbolic;
symbols: '*' ⁑ † ‡;
suffix: '';
}`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@counter-style",
Prelude: "footnote",
Declarations: []*css.Declaration{
{
Property: "system",
Value: "symbolic",
Line: 2,
Column: 3,
},
{
Property: "symbols",
Value: "'*' ⁑ † ‡",
Line: 3,
Column: 3,
},
{
Property: "suffix",
Value: "''",
Line: 4,
Column: 3,
},
},
}
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), input)
}
func TestAtRuleDocument(t *testing.T) {
input := `@document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*")
{
/* CSS rules here apply to:
+ The page "http://www.w3.org/".
+ Any page whose URL begins with "http://www.w3.org/Style/"
+ Any page whose URL's host is "mozilla.org" or ends with
".mozilla.org"
+ Any page whose URL starts with "https:" */
/* make the above-mentioned pages really ugly */
body { color: purple; background: yellow; }
}`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@document",
Prelude: `url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*")`,
Rules: []*css.Rule{
{
Kind: css.QualifiedRule,
Prelude: "body",
Selectors: []*css.Selector{
{
Value: "body",
Line: 14,
Column: 3,
},
},
Declarations: []*css.Declaration{
{
Property: "color",
Value: "purple",
Line: 14,
Column: 10,
},
{
Property: "background",
Value: "yellow",
Line: 14,
Column: 25,
},
},
},
},
}
expectCSS := `@document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*") {
body {
color: purple;
background: yellow;
}
}`
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), expectCSS)
}
func TestAtRuleFontFace(t *testing.T) {
input := `@font-face {
font-family: MyHelvetica;
src: local("Helvetica Neue Bold"),
local("HelveticaNeue-Bold"),
url(MgOpenModernaBold.ttf);
font-weight: bold;
}`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@font-face",
Declarations: []*css.Declaration{
{
Property: "font-family",
Value: "MyHelvetica",
Line: 2,
Column: 3,
},
{
Property: "src",
Value: `local("Helvetica Neue Bold"),
local("HelveticaNeue-Bold"),
url(MgOpenModernaBold.ttf)`,
Line: 3,
Column: 3,
},
{
Property: "font-weight",
Value: "bold",
Line: 6,
Column: 3,
},
},
}
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), input)
}
func TestAtRuleFontFeatureValues(t *testing.T) {
input := `@font-feature-values Font Two { /* How to activate nice-style in Font Two */
@styleset {
nice-style: 4;
}
}`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@font-feature-values",
Prelude: "Font Two",
Rules: []*css.Rule{
{
Kind: css.AtRule,
Name: "@styleset",
Declarations: []*css.Declaration{
{
Property: "nice-style",
Value: "4",
Line: 3,
Column: 5,
},
},
},
},
}
expectedOutput := `@font-feature-values Font Two {
@styleset {
nice-style: 4;
}
}`
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), expectedOutput)
}
func TestAtRuleImport(t *testing.T) {
input := `@import "my-styles.css";
@import url('landscape.css') screen and (orientation:landscape);`
expectedRule1 := &css.Rule{
Kind: css.AtRule,
Name: "@import",
Prelude: "\"my-styles.css\"",
}
expectedRule2 := &css.Rule{
Kind: css.AtRule,
Name: "@import",
Prelude: "url('landscape.css') screen and (orientation:landscape)",
}
stylesheet := MustParse(t, input, 2)
MustEqualRule(t, stylesheet.Rules[0], expectedRule1)
MustEqualRule(t, stylesheet.Rules[1], expectedRule2)
MustEqualCSS(t, stylesheet.String(), input)
}
func TestAtRuleKeyframes(t *testing.T) {
input := `@keyframes identifier {
0% { top: 0; left: 0; }
100% { top: 100px; left: 100%; }
}`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@keyframes",
Prelude: "identifier",
Rules: []*css.Rule{
{
Kind: css.QualifiedRule,
Prelude: "0%",
Selectors: []*css.Selector{
{
Value: "0%",
Line: 2,
Column: 3,
},
},
Declarations: []*css.Declaration{
{
Property: "top",
Value: "0",
Line: 2,
Column: 8,
},
{
Property: "left",
Value: "0",
Line: 2,
Column: 16,
},
},
},
{
Kind: css.QualifiedRule,
Prelude: "100%",
Selectors: []*css.Selector{
{
Value: "100%",
Line: 3,
Column: 3,
},
},
Declarations: []*css.Declaration{
{
Property: "top",
Value: "100px",
Line: 3,
Column: 10,
},
{
Property: "left",
Value: "100%",
Line: 3,
Column: 22,
},
},
},
},
}
expectedOutput := `@keyframes identifier {
0% {
top: 0;
left: 0;
}
100% {
top: 100px;
left: 100%;
}
}`
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), expectedOutput)
}
func TestAtRuleMedia(t *testing.T) {
input := `@media screen, print {
body { line-height: 1.2 }
}`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@media",
Prelude: "screen, print",
Rules: []*css.Rule{
{
Kind: css.QualifiedRule,
Prelude: "body",
Selectors: []*css.Selector{
{
Value: "body",
Line: 2,
Column: 3,
},
},
Declarations: []*css.Declaration{
{
Property: "line-height",
Value: "1.2",
Line: 2,
Column: 10,
},
},
},
},
}
expectedOutput := `@media screen, print {
body {
line-height: 1.2;
}
}`
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), expectedOutput)
}
func TestAtRuleNamespace(t *testing.T) {
input := `@namespace svg url(http://www.w3.org/2000/svg);`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@namespace",
Prelude: "svg url(http://www.w3.org/2000/svg)",
}
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), input)
}
func TestAtRulePage(t *testing.T) {
input := `@page :left {
margin-left: 4cm;
margin-right: 3cm;
}`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@page",
Prelude: ":left",
Declarations: []*css.Declaration{
{
Property: "margin-left",
Value: "4cm",
Line: 2,
Column: 3,
},
{
Property: "margin-right",
Value: "3cm",
Line: 3,
Column: 3,
},
},
}
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), input)
}
func TestAtRuleSupports(t *testing.T) {
input := `@supports (animation-name: test) {
/* specific CSS applied when animations are supported unprefixed */
@keyframes { /* @supports being a CSS conditional group at-rule, it can includes other relevent at-rules */
0% { top: 0; left: 0; }
100% { top: 100px; left: 100%; }
}
}`
expectedRule := &css.Rule{
Kind: css.AtRule,
Name: "@supports",
Prelude: "(animation-name: test)",
Rules: []*css.Rule{
{
Kind: css.AtRule,
Name: "@keyframes",
Rules: []*css.Rule{
{
Kind: css.QualifiedRule,
Prelude: "0%",
Selectors: []*css.Selector{
{
Value: "0%",
Line: 4,
Column: 7,
},
},
Declarations: []*css.Declaration{
{
Property: "top",
Value: "0",
Line: 4,
Column: 12,
},
{
Property: "left",
Value: "0",
Line: 4,
Column: 20,
},
},
},
{
Kind: css.QualifiedRule,
Prelude: "100%",
Selectors: []*css.Selector{
{
Value: "100%",
Line: 5,
Column: 7,
},
},
Declarations: []*css.Declaration{
{
Property: "top",
Value: "100px",
Line: 5,
Column: 14,
},
{
Property: "left",
Value: "100%",
Line: 5,
Column: 26,
},
},
},
},
},
},
}
expectedOutput := `@supports (animation-name: test) {
@keyframes {
0% {
top: 0;
left: 0;
}
100% {
top: 100px;
left: 100%;
}
}
}`
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
MustEqualCSS(t, stylesheet.String(), expectedOutput)
}
func TestParseDeclarations(t *testing.T) {
input := `color: blue; text-decoration:underline;`
declarations, err := ParseDeclarations(input)
if err != nil {
t.Fatal("Failed to parse Declarations:", input)
}
expectedOutput := []*css.Declaration{
{
Property: "color",
Value: "blue",
Line: 1,
Column: 1,
},
{
Property: "text-decoration",
Value: "underline",
Line: 1,
Column: 14,
},
}
if len(declarations) != len(expectedOutput) {
t.Fatal("Failed to parse Declarations:", input)
}
for i, decl := range declarations {
if !decl.Equal(expectedOutput[i]) {
t.Fatal("Failed to parse Declarations: ", decl.Str(true), expectedOutput[i].Str(true))
}
}
}
func TestMultipleDeclarations(t *testing.T) {
input := `.btn:focus,
.btn:active:focus,
.btn.active:focus,
.btn.focus,
.btn:active.focus,
.btn.active.focus {
}`
expectedRule := &css.Rule{
Kind: css.QualifiedRule,
Prelude: `.btn:focus,
.btn:active:focus,
.btn.active:focus,
.btn.focus,
.btn:active.focus,
.btn.active.focus`,
Selectors: []*css.Selector{
{
Value: ".btn:focus",
Line: 1,
Column: 1,
},
{
Value: ".btn:active:focus",
Line: 2,
Column: 1,
},
{
Value: ".btn.active:focus",
Line: 3,
Column: 1,
},
{
Value: ".btn.focus",
Line: 4,
Column: 1,
},
{
Value: ".btn:active.focus",
Line: 5,
Column: 1,
},
{
Value: ".btn.active.focus",
Line: 6,
Column: 1,
},
},
Declarations: []*css.Declaration{},
}
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
}
func TestComments(t *testing.T) {
input := "td /* © */ { color /* © */: red; }"
expectedRule := &css.Rule{
Kind: css.QualifiedRule,
Prelude: "td /* © */",
Selectors: []*css.Selector{
{
Value: "td",
Line: 1,
Column: 1,
},
},
Declarations: []*css.Declaration{
{
Property: "color",
Value: "red",
Line: 1,
Column: 14,
},
},
}
stylesheet := MustParse(t, input, 1)
rule := stylesheet.Rules[0]
MustEqualRule(t, rule, expectedRule)
}
func TestInfiniteLoop(t *testing.T) {
input := "{;}"
_, err := Parse(input)
if err == nil {
t.Fatal("Expected an error got nil")
}
}