pax_global_header00006660000000000000000000000064125676233330014524gustar00rootroot0000000000000052 comment=c5c95ec357c8235fbd7f34e8c843d36783f3fad9 douceur-0.2.0/000077500000000000000000000000001256762333300131715ustar00rootroot00000000000000douceur-0.2.0/.gitignore000066400000000000000000000000211256762333300151520ustar00rootroot00000000000000douceur test.htmldouceur-0.2.0/.travis.yml000066400000000000000000000001161256762333300153000ustar00rootroot00000000000000--- language: go go: - 1.3 - tip script: - go build - go test ./... douceur-0.2.0/CHANGELOG.md000066400000000000000000000004371256762333300150060ustar00rootroot00000000000000# Douceur Changelog ### Douceur 0.2.0 _(August 27, 2015)_ - Applied vet and lint on all source code. - [BREAKING CHANGE] Some const were renamed, and even unexported. ### Douceur 0.1.0 _(April 15, 2015)_ - First release. Fetching of external stylesheets is the main missing feature. douceur-0.2.0/LICENSE000066400000000000000000000020741256762333300142010ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Aymerick JEHANNE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. douceur-0.2.0/README.md000066400000000000000000000063101256762333300144500ustar00rootroot00000000000000# douceur [![Build Status](https://secure.travis-ci.org/aymerick/douceur.svg?branch=master)](http://travis-ci.org/aymerick/douceur) A simple CSS parser and inliner in Golang. ![Douceur Logo](https://github.com/aymerick/douceur/blob/master/douceur.png?raw=true "Douceur") Parser is vaguely inspired by [CSS Syntax Module Level 3](http://www.w3.org/TR/css3-syntax) and [corresponding JS parser](https://github.com/tabatkins/parse-css). Inliner only parses CSS defined in HTML document, it *DOES NOT* fetch external stylesheets (for now). Inliner inserts additional attributes when possible, for example: ```html

Inline me !

` ``` Becomes: ```html

Inline me !

` ``` The `bgcolor` attribute is inserted, in addition to the inlined `background-color` style. ## Tool usage Install tool: $ go install github.com/aymerick/douceur Parse a CSS file and display result: $ douceur parse inputfile.css Inline CSS in an HTML document and display result: $ douceur inline inputfile.html ## Library usage Fetch package: $ go get github.com/aymerick/douceur ### Parse CSS ```go package main import ( "fmt" "github.com/aymerick/douceur/parser" ) func main() { input := `body { /* D4rK s1T3 */ background-color: black; } p { /* Try to read that ! HAHA! */ color: red; /* L O L */ } ` stylesheet, err := parser.Parse(input) if err != nil { panic("Please fill a bug :)") } fmt.Print(stylesheet.String()) } ``` Displays: ```css body { background-color: black; } p { color: red; } ``` ### Inline HTML ```go package main import ( "fmt" "github.com/aymerick/douceur/inliner" ) func main() { input := `

Inline me please!

` html, err := inliner.Inline(input) if err != nil { panic("Please fill a bug :)") } fmt.Print(html) } ``` Displays: ```css

Inline me please!

``` ## Test go test ./... -v ## Dependencies - Parser uses [Gorilla CSS3 tokenizer](https://github.com/gorilla/css). - Inliner uses [goquery](github.com/PuerkitoBio/goquery) to manipulate HTML. ## Similar projects - [premailer](https://github.com/premailer/premailer) - [roadie](https://github.com/Mange/roadie) douceur-0.2.0/css/000077500000000000000000000000001256762333300137615ustar00rootroot00000000000000douceur-0.2.0/css/declaration.go000066400000000000000000000027501256762333300166010ustar00rootroot00000000000000package css import "fmt" // Declaration represents a parsed style property type Declaration struct { Property string Value string Important bool } // NewDeclaration instanciates a new Declaration func NewDeclaration() *Declaration { return &Declaration{} } // Returns string representation of the Declaration func (decl *Declaration) String() string { return decl.StringWithImportant(true) } // StringWithImportant returns string representation with optional !important part func (decl *Declaration) StringWithImportant(option bool) string { result := fmt.Sprintf("%s: %s", decl.Property, decl.Value) if option && decl.Important { result += " !important" } result += ";" return result } // Equal returns true if both Declarations are equals func (decl *Declaration) Equal(other *Declaration) bool { return (decl.Property == other.Property) && (decl.Value == other.Value) && (decl.Important == other.Important) } // // DeclarationsByProperty // // DeclarationsByProperty represents sortable style declarations type DeclarationsByProperty []*Declaration // Implements sort.Interface func (declarations DeclarationsByProperty) Len() int { return len(declarations) } // Implements sort.Interface func (declarations DeclarationsByProperty) Swap(i, j int) { declarations[i], declarations[j] = declarations[j], declarations[i] } // Implements sort.Interface func (declarations DeclarationsByProperty) Less(i, j int) bool { return declarations[i].Property < declarations[j].Property } douceur-0.2.0/css/rule.go000066400000000000000000000115271256762333300152650ustar00rootroot00000000000000package css import ( "fmt" "strings" ) const ( indentSpace = 2 ) // RuleKind represents a Rule kind type RuleKind int // Rule kinds const ( QualifiedRule RuleKind = iota AtRule ) // At Rules than have Rules inside their block instead of Declarations var atRulesWithRulesBlock = []string{ "@document", "@font-feature-values", "@keyframes", "@media", "@supports", } // Rule represents a parsed CSS rule type Rule struct { Kind RuleKind // At Rule name (eg: "@media") Name string // Raw prelude Prelude string // Qualified Rule selectors parsed from prelude Selectors []string // Style properties Declarations []*Declaration // At Rule embedded rules Rules []*Rule // Current rule embedding level EmbedLevel int } // NewRule instanciates a new Rule func NewRule(kind RuleKind) *Rule { return &Rule{ Kind: kind, } } // Returns string representation of rule kind func (kind RuleKind) String() string { switch kind { case QualifiedRule: return "Qualified Rule" case AtRule: return "At Rule" default: return "WAT" } } // EmbedsRules returns true if this rule embeds another rules func (rule *Rule) EmbedsRules() bool { if rule.Kind == AtRule { for _, atRuleName := range atRulesWithRulesBlock { if rule.Name == atRuleName { return true } } } return false } // Equal returns true if both rules are equals func (rule *Rule) Equal(other *Rule) bool { if (rule.Kind != other.Kind) || (rule.Prelude != other.Prelude) || (rule.Name != other.Name) { return false } if (len(rule.Selectors) != len(other.Selectors)) || (len(rule.Declarations) != len(other.Declarations)) || (len(rule.Rules) != len(other.Rules)) { return false } for i, sel := range rule.Selectors { if sel != other.Selectors[i] { return false } } for i, decl := range rule.Declarations { if !decl.Equal(other.Declarations[i]) { return false } } for i, rule := range rule.Rules { if !rule.Equal(other.Rules[i]) { return false } } return true } // Diff returns a string representation of rules differences func (rule *Rule) Diff(other *Rule) []string { result := []string{} if rule.Kind != other.Kind { result = append(result, fmt.Sprintf("Kind: %s | %s", rule.Kind.String(), other.Kind.String())) } if rule.Prelude != other.Prelude { result = append(result, fmt.Sprintf("Prelude: \"%s\" | \"%s\"", rule.Prelude, other.Prelude)) } if rule.Name != other.Name { result = append(result, fmt.Sprintf("Name: \"%s\" | \"%s\"", rule.Name, other.Name)) } if len(rule.Selectors) != len(other.Selectors) { result = append(result, fmt.Sprintf("Selectors: %v | %v", strings.Join(rule.Selectors, ", "), strings.Join(other.Selectors, ", "))) } else { for i, sel := range rule.Selectors { if sel != other.Selectors[i] { result = append(result, fmt.Sprintf("Selector: \"%s\" | \"%s\"", sel, other.Selectors[i])) } } } if len(rule.Declarations) != len(other.Declarations) { result = append(result, fmt.Sprintf("Declarations Nb: %d | %d", len(rule.Declarations), len(other.Declarations))) } else { for i, decl := range rule.Declarations { if !decl.Equal(other.Declarations[i]) { result = append(result, fmt.Sprintf("Declaration: \"%s\" | \"%s\"", decl.String(), other.Declarations[i].String())) } } } if len(rule.Rules) != len(other.Rules) { result = append(result, fmt.Sprintf("Rules Nb: %d | %d", len(rule.Rules), len(other.Rules))) } else { for i, rule := range rule.Rules { if !rule.Equal(other.Rules[i]) { result = append(result, fmt.Sprintf("Rule: \"%s\" | \"%s\"", rule.String(), other.Rules[i].String())) } } } return result } // Returns the string representation of a rule func (rule *Rule) String() string { result := "" if rule.Kind == QualifiedRule { for i, sel := range rule.Selectors { if i != 0 { result += ", " } result += sel } } else { // AtRule result += fmt.Sprintf("%s", rule.Name) if rule.Prelude != "" { if result != "" { result += " " } result += fmt.Sprintf("%s", rule.Prelude) } } if (len(rule.Declarations) == 0) && (len(rule.Rules) == 0) { result += ";" } else { result += " {\n" if rule.EmbedsRules() { for _, subRule := range rule.Rules { result += fmt.Sprintf("%s%s\n", rule.indent(), subRule.String()) } } else { for _, decl := range rule.Declarations { result += fmt.Sprintf("%s%s\n", rule.indent(), decl.String()) } } result += fmt.Sprintf("%s}", rule.indentEndBlock()) } return result } // Returns identation spaces for declarations and rules func (rule *Rule) indent() string { result := "" for i := 0; i < ((rule.EmbedLevel + 1) * indentSpace); i++ { result += " " } return result } // Returns identation spaces for end of block character func (rule *Rule) indentEndBlock() string { result := "" for i := 0; i < (rule.EmbedLevel * indentSpace); i++ { result += " " } return result } douceur-0.2.0/css/stylesheet.go000066400000000000000000000006711256762333300165050ustar00rootroot00000000000000package css // Stylesheet represents a parsed stylesheet type Stylesheet struct { Rules []*Rule } // NewStylesheet instanciate a new Stylesheet func NewStylesheet() *Stylesheet { return &Stylesheet{} } // Returns string representation of the Stylesheet func (sheet *Stylesheet) String() string { result := "" for _, rule := range sheet.Rules { if result != "" { result += "\n" } result += rule.String() } return result } douceur-0.2.0/douceur.go000066400000000000000000000027261256762333300151750ustar00rootroot00000000000000package main import ( "flag" "fmt" "io/ioutil" "os" "github.com/aymerick/douceur/inliner" "github.com/aymerick/douceur/parser" ) const ( // Version is package version Version = "0.2.0" ) var ( flagVersion bool ) func init() { flag.BoolVar(&flagVersion, "version", false, "Display version") } func main() { flag.Parse() if flagVersion { fmt.Println(Version) os.Exit(0) } args := flag.Args() if len(args) == 0 { fmt.Println("No command supplied") os.Exit(1) } switch args[0] { case "parse": if len(args) < 2 { fmt.Println("Missing file path") os.Exit(1) } parseCSS(args[1]) case "inline": if len(args) < 2 { fmt.Println("Missing file path") os.Exit(1) } inlineCSS(args[1]) default: fmt.Println("Unexpected command: ", args[0]) os.Exit(1) } } // parse and display CSS file func parseCSS(filePath string) { input := readFile(filePath) stylesheet, err := parser.Parse(string(input)) if err != nil { fmt.Println("Parsing error: ", err) os.Exit(1) } fmt.Println(stylesheet.String()) } // inlines CSS into HTML and display result func inlineCSS(filePath string) { input := readFile(filePath) output, err := inliner.Inline(string(input)) if err != nil { fmt.Println("Inlining error: ", err) os.Exit(1) } fmt.Println(output) } func readFile(filePath string) []byte { file, err := ioutil.ReadFile(filePath) if err != nil { fmt.Println("Failed to open file: ", filePath, err) os.Exit(1) } return file } douceur-0.2.0/douceur.png000066400000000000000000000151671256762333300153570ustar00rootroot00000000000000PNG  IHDRPPgAMA a cHRMz&u0`:pQ< pHYs  iTXtXML:com.adobe.xmp 1 1 2 ؀ IDATxI]Uv}`ZwӃ 1!xOUBD2F jH*L*  e(* %?`07lܽkZl|鮻޾} 70?44(SUBR]G7ź_k,TCmmߢW\q׉.;cts\sl^7ͽ4l_~<΃>JJ_"'TRosxx!R{C.loUm5oV>?]~O]|CY~O{@7"g,J񫔯;w ̪-T297hTͱu+|ޝ2~yUU9+)ʿBpC "\KC㫍6q°p9::ꫯchoK}C{ҵ6555lJu~;v`!Ӫ\a1yR{;2$x;N7E7Or?B5NLLVo`wz'a \'C(&cQ=ù^T&~ )[Ȱ'zAZ~r?( H7Ja2U`t_P\g>eu2d#]۾ɕyA(8pbve˖]|}Y{ALOQ*#bVrrW:hd>TLĢ]8wcm0y|N}:ãuX.׵QWW_3,D?"ydda-@A'̨IP4w+7N{w׫|/M ~=#Ƥc+(۳4+-WL`B~`1g[g36<\SxaoZ/b1z`@8Ȫ:If!xBu8doV:dqjޏIʚ:(Lp2Յa]"+t$Ł@+~dIE29+/f:YLYe:y/Շ+뫂Y lbɃ7bbR=W1^@IH(G" e9$mS^923ȧ4'YW1 G(oP8?UDn?qG$|Ͱ|`Uz󆉀+VX8|BS0|u(ÆoT:MHa݌ -|C=*8& J3`7/V1cHe+'JVy]'!>^v:$yyG% $A˃xAP_6xpX2val9\c;_iZ*Fs \BbjsZyքrb[/  7#yq?s(I A?8KU9xx0F<'tӌr> Kl~vaiWq%T=Zй|N?ҥK؆,:n*'ƂÌ hXEb 1,#V駟\dJ3 7U/!A|OfP>9;_&e,yP7e mzjCgaX9^W_ @,ffEf|n\G3jD[J #%rSmMCbQF,4rvJ,BaY]N U.Y +(\.[j-הE=܌X&9VA޲eKSt1 Y6K R.+:g1>,iŭYa$'`IuO>dɒa ]Qu`-7,рe{g;@lr(Cb""O~~Pz|O`uzkk?U@_VOOorA/=Z1- ЦI`LXN fzO(ehUN=MW-r3Nyy'H7mNI֑X͛ mb$>1[c6P!P Kfw3aoBsy^,OjxK.bBUW| !y[=- {Xs=74[")0ClÈOlê; t (np}B c>u?˒kVX/~;UNi_@+QnAM9JzG<2!pPN̺TX cM9 $8E9deaZYxs2cSyr(1H DGb /l nj1Qc%.4E= da'/0ufr@yX(p#`fԗ>$7ޤ'w pR̤Qw^ħtxB`xxGlMW'շgCB?N/N.f56XkKJ ", aWS)0WrZbWyVMk^O?_Ay}T>rnS}9 e%ݯ+0/"jzvW]바ٖ-9e&ܳ}"> >[轑e@ ۘYRJF͖_.ƺub{$C=p/ŃQƫW{CԒUtgۂU&m zWZ%^1`uPLIxV-:%w}ut>!fMQ+&''ca(GF)VoU@<+fl1݅4>$#L4`Wi<aXě>d,ڴ1?fNDj;67[ iZ[")}ܻLdw;J۲~m*o c崢ߩӘݷǔE_\ kNˏYg9i\M0 S/枤v>?Jo:]7& W>!?X螉cL_Bm^wOLLNI }gY'^x3+&Z+ׅL~LDr`/5>j.nT#=]t< -_G0%pSp bP@O>wBK|<#'7eb/_9u^=@wfz{諩zz^:Acko9AT;m\3 5:ĶO>,d%SYA>r.F3E֫_XIGshW\U6 %Ć=}@"ĠwbSWW_Ы +Je2^a:bڟ5C ċ.Z 3E9w7ѫq%vk* m0o*]zw/υ*'xtQ.~Tln|V d5MZ69Kd]vlȊȎ0 v aL\0QBp{K[.aN#%cȌ.> T)i֓ ݬW{>J1D9'wZ]A RV\%RN8_ub+6{ҞZ$$Tk%j"MAFyQEN2ʮS5gu/dqW4XEZ;jῈۊ9ݚ:1_o;3D1|%䃃Z.ǥJ8"q8t-nV?#"m  ~?i:Q@|YY!0a/].rDIENDB`douceur-0.2.0/inliner/000077500000000000000000000000001256762333300146315ustar00rootroot00000000000000douceur-0.2.0/inliner/element.go000066400000000000000000000101721256762333300166120ustar00rootroot00000000000000package inliner import ( "sort" "github.com/PuerkitoBio/goquery" "github.com/aymerick/douceur/css" "github.com/aymerick/douceur/parser" ) // Element represents a HTML element with matching CSS rules type Element struct { // The goquery handler elt *goquery.Selection // The style rules to apply on that element styleRules []*StyleRule } // ElementAttr represents a HTML element attribute type ElementAttr struct { attr string elements []string } // Index is style property name var styleToAttr map[string]*ElementAttr func init() { // Borrowed from premailer: // https://github.com/premailer/premailer/blob/master/lib/premailer/premailer.rb styleToAttr = map[string]*ElementAttr{ "text-align": &ElementAttr{ "align", []string{"h1", "h2", "h3", "h4", "h5", "h6", "p", "div", "blockquote", "tr", "th", "td"}, }, "background-color": &ElementAttr{ "bgcolor", []string{"body", "table", "tr", "th", "td"}, }, "background-image": &ElementAttr{ "background", []string{"table"}, }, "vertical-align": &ElementAttr{ "valign", []string{"th", "td"}, }, "float": &ElementAttr{ "align", []string{"img"}, }, // @todo width and height ? } } // NewElement instanciates a new element func NewElement(elt *goquery.Selection) *Element { return &Element{ elt: elt, } } // Add a Style Rule to Element func (element *Element) addStyleRule(styleRule *StyleRule) { element.styleRules = append(element.styleRules, styleRule) } // Inline styles on element func (element *Element) inline() error { // compute declarations declarations, err := element.computeDeclarations() if err != nil { return err } // set style attribute styleValue := computeStyleValue(declarations) if styleValue != "" { element.elt.SetAttr("style", styleValue) } // set additionnal attributes element.setAttributesFromStyle(declarations) return nil } // Compute css declarations func (element *Element) computeDeclarations() ([]*css.Declaration, error) { result := []*css.Declaration{} styles := make(map[string]*StyleDeclaration) // First: parsed stylesheets rules mergeStyleDeclarations(element.styleRules, styles) // Then: inline rules inlineRules, err := element.parseInlineStyle() if err != nil { return result, err } mergeStyleDeclarations(inlineRules, styles) // map to array for _, styleDecl := range styles { result = append(result, styleDecl.Declaration) } // sort declarations by property name sort.Sort(css.DeclarationsByProperty(result)) return result, nil } // Parse inline style rules func (element *Element) parseInlineStyle() ([]*StyleRule, error) { result := []*StyleRule{} styleValue, exists := element.elt.Attr("style") if (styleValue == "") || !exists { return result, nil } declarations, err := parser.ParseDeclarations(styleValue) if err != nil { return result, err } result = append(result, NewStyleRule(inlineFakeSelector, declarations)) return result, nil } // Set additional attributes from style declarations func (element *Element) setAttributesFromStyle(declarations []*css.Declaration) { // for each style declarations for _, declaration := range declarations { if eltAttr := styleToAttr[declaration.Property]; eltAttr != nil { // check if element is allowed for that attribute for _, eltAllowed := range eltAttr.elements { if element.elt.Nodes[0].Data == eltAllowed { element.elt.SetAttr(eltAttr.attr, declaration.Value) break } } } } } // helper func computeStyleValue(declarations []*css.Declaration) string { result := "" // set style attribute value for _, declaration := range declarations { if result != "" { result += " " } result += declaration.StringWithImportant(false) } return result } // helper func mergeStyleDeclarations(styleRules []*StyleRule, output map[string]*StyleDeclaration) { for _, styleRule := range styleRules { for _, declaration := range styleRule.Declarations { styleDecl := NewStyleDeclaration(styleRule, declaration) if (output[declaration.Property] == nil) || (styleDecl.Specificity() >= output[declaration.Property].Specificity()) { output[declaration.Property] = styleDecl } } } } douceur-0.2.0/inliner/inliner.go000066400000000000000000000122771256762333300166310ustar00rootroot00000000000000package inliner import ( "fmt" "strconv" "strings" "github.com/PuerkitoBio/goquery" "github.com/aymerick/douceur/css" "github.com/aymerick/douceur/parser" "golang.org/x/net/html" ) const ( eltMarkerAttr = "douceur-mark" ) var unsupportedSelectors = []string{ ":active", ":after", ":before", ":checked", ":disabled", ":enabled", ":first-line", ":first-letter", ":focus", ":hover", ":invalid", ":in-range", ":lang", ":link", ":root", ":selection", ":target", ":valid", ":visited"} // Inliner presents a CSS Inliner type Inliner struct { // Raw HTML html string // Parsed HTML document doc *goquery.Document // Parsed stylesheets stylesheets []*css.Stylesheet // Collected inlinable style rules rules []*StyleRule // HTML elements matching collected inlinable style rules elements map[string]*Element // CSS rules that are not inlinable but that must be inserted in output document rawRules []fmt.Stringer // current element marker value eltMarker int } // NewInliner instanciates a new Inliner func NewInliner(html string) *Inliner { return &Inliner{ html: html, elements: make(map[string]*Element), } } // Inline inlines css into html document func Inline(html string) (string, error) { result, err := NewInliner(html).Inline() if err != nil { return "", err } return result, nil } // Inline inlines CSS and returns HTML func (inliner *Inliner) Inline() (string, error) { // parse HTML document if err := inliner.parseHTML(); err != nil { return "", err } // parse stylesheets if err := inliner.parseStylesheets(); err != nil { return "", err } // collect elements and style rules inliner.collectElementsAndRules() // inline css if err := inliner.inlineStyleRules(); err != nil { return "", err } // insert raw stylesheet inliner.insertRawStylesheet() // generate HTML document return inliner.genHTML() } // Parses raw html func (inliner *Inliner) parseHTML() error { doc, err := goquery.NewDocumentFromReader(strings.NewReader(inliner.html)) if err != nil { return err } inliner.doc = doc return nil } // Parses and removes stylesheets from HTML document func (inliner *Inliner) parseStylesheets() error { var result error inliner.doc.Find("style").EachWithBreak(func(i int, s *goquery.Selection) bool { stylesheet, err := parser.Parse(s.Text()) if err != nil { result = err return false } inliner.stylesheets = append(inliner.stylesheets, stylesheet) // removes parsed stylesheet s.Remove() return true }) return result } // Collects HTML elements matching parsed stylesheets, and thus collect used style rules func (inliner *Inliner) collectElementsAndRules() { for _, stylesheet := range inliner.stylesheets { for _, rule := range stylesheet.Rules { if rule.Kind == css.QualifiedRule { // Let's go! inliner.handleQualifiedRule(rule) } else { // Keep it 'as is' inliner.rawRules = append(inliner.rawRules, rule) } } } } // Handles parsed qualified rule func (inliner *Inliner) handleQualifiedRule(rule *css.Rule) { for _, selector := range rule.Selectors { if Inlinable(selector) { inliner.doc.Find(selector).Each(func(i int, s *goquery.Selection) { // get marker eltMarker, exists := s.Attr(eltMarkerAttr) if !exists { // mark element eltMarker = strconv.Itoa(inliner.eltMarker) s.SetAttr(eltMarkerAttr, eltMarker) inliner.eltMarker++ // add new element inliner.elements[eltMarker] = NewElement(s) } // add style rule for element inliner.elements[eltMarker].addStyleRule(NewStyleRule(selector, rule.Declarations)) }) } else { // Keep it 'as is' inliner.rawRules = append(inliner.rawRules, NewStyleRule(selector, rule.Declarations)) } } } // Inline style rules in HTML document func (inliner *Inliner) inlineStyleRules() error { for _, element := range inliner.elements { // remove marker element.elt.RemoveAttr(eltMarkerAttr) // inline element err := element.inline() if err != nil { return err } } return nil } // Computes raw CSS rules func (inliner *Inliner) computeRawCSS() string { result := "" for _, rawRule := range inliner.rawRules { result += rawRule.String() result += "\n" } return result } // Insert raw CSS rules into HTML document func (inliner *Inliner) insertRawStylesheet() { rawCSS := inliner.computeRawCSS() if rawCSS != "" { // create

Inline me please!

` expectedOutput := `

Inline me please!

` output, err := Inline(input) if err != nil { t.Fatal("Failed to inline html:", err) } if output != expectedOutput { t.Fatal(fmt.Sprintf("CSS inliner error\nExpected:\n\"%s\"\nGot:\n\"%s\"", expectedOutput, output)) } } // Already inlined style has more priority than

Inline me please!

` expectedOutput := `

Inline me please!

` output, err := Inline(input) if err != nil { t.Fatal("Failed to inline html:", err) } if output != expectedOutput { t.Fatal(fmt.Sprintf("CSS inliner error\nExpected:\n\"%s\"\nGot:\n\"%s\"", expectedOutput, output)) } } // !important has highest priority func TestImportantPriority(t *testing.T) { input := `

Inline me please!

` expectedOutput := `

Inline me please!

` output, err := Inline(input) if err != nil { t.Fatal("Failed to inline html:", err) } if output != expectedOutput { t.Fatal(fmt.Sprintf("CSS inliner error\nExpected:\n\"%s\"\nGot:\n\"%s\"", expectedOutput, output)) } } // Pseudo-class selectors can't be inlined func TestNotInlinable(t *testing.T) { input := `

Superbe website

` expectedOutput := `

Superbe website

` output, err := Inline(input) if err != nil { t.Fatal("Failed to inline html:", err) } if output != expectedOutput { t.Fatal(fmt.Sprintf("CSS inliner error\nExpected:\n\"%s\"\nGot:\n\"%s\"", expectedOutput, output)) } } // Some styles causes insertion of additional element attributes func TestStyleToAttr(t *testing.T) { input := `

test

test

test

test

test
test

test
test
` expectedOutput := `

test

test

test

test

test
test

test
test
` output, err := Inline(input) if err != nil { t.Fatal("Failed to inline html:", err) } if output != expectedOutput { t.Fatal(fmt.Sprintf("CSS inliner error\nExpected:\n\"%s\"\nGot:\n\"%s\"", expectedOutput, output)) } } douceur-0.2.0/inliner/style_declaration.go000066400000000000000000000012161256762333300206650ustar00rootroot00000000000000package inliner import "github.com/aymerick/douceur/css" // StyleDeclaration represents a style declaration type StyleDeclaration struct { StyleRule *StyleRule Declaration *css.Declaration } // NewStyleDeclaration instanciates a new StyleDeclaration func NewStyleDeclaration(styleRule *StyleRule, declaration *css.Declaration) *StyleDeclaration { return &StyleDeclaration{ StyleRule: styleRule, Declaration: declaration, } } // StyleDeclaration computes style declaration specificity func (styleDecl *StyleDeclaration) Specificity() int { if styleDecl.Declaration.Important { return 10000 } return styleDecl.StyleRule.Specificity } douceur-0.2.0/inliner/style_rule.go000066400000000000000000000044541256762333300173560ustar00rootroot00000000000000package inliner import ( "fmt" "regexp" "strings" "github.com/aymerick/douceur/css" ) const ( inlineFakeSelector = "*INLINE*" // Regular expressions borrowed from premailer: // https://github.com/premailer/css_parser/blob/master/lib/css_parser/regexps.rb nonIDAttributesAndPseudoClassesRegexpConst = `(?i)(\.[\w]+)|\[(\w+)|(\:(link|visited|active|hover|focus|lang|target|enabled|disabled|checked|indeterminate|root|nth-child|nth-last-child|nth-of-type|nth-last-of-type|first-child|last-child|first-of-type|last-of-type|only-child|only-of-type|empty|contains))` elementsAndPseudoElementsRegexpConst = `(?i)((^|[\s\+\>\~]+)[\w]+|\:{1,2}(after|before|first-letter|first-line|selection))` ) var ( nonIDAttrAndPseudoClassesRegexp *regexp.Regexp elementsAndPseudoElementsRegexp *regexp.Regexp ) // StyleRule represents a Qualifier Rule for a uniq selector type StyleRule struct { // The style rule selector Selector string // The style rule properties Declarations []*css.Declaration // Selector specificity Specificity int } func init() { nonIDAttrAndPseudoClassesRegexp, _ = regexp.Compile(nonIDAttributesAndPseudoClassesRegexpConst) elementsAndPseudoElementsRegexp, _ = regexp.Compile(elementsAndPseudoElementsRegexpConst) } // NewStyleRule instanciates a new StyleRule func NewStyleRule(selector string, declarations []*css.Declaration) *StyleRule { return &StyleRule{ Selector: selector, Declarations: declarations, Specificity: ComputeSpecificity(selector), } } // Returns the string representation of a style rule func (styleRule *StyleRule) String() string { result := "" result += styleRule.Selector if len(styleRule.Declarations) == 0 { result += ";" } else { result += " {\n" for _, decl := range styleRule.Declarations { result += fmt.Sprintf(" %s\n", decl.String()) } result += "}" } return result } // ComputeSpecificity computes style rule specificity // // cf. http://www.w3.org/TR/selectors/#specificity func ComputeSpecificity(selector string) int { result := 0 if selector == inlineFakeSelector { result += 1000 } result += 100 * strings.Count(selector, "#") result += 10 * len(nonIDAttrAndPseudoClassesRegexp.FindAllStringSubmatch(selector, -1)) result += len(elementsAndPseudoElementsRegexp.FindAllStringSubmatch(selector, -1)) return result } douceur-0.2.0/inliner/style_rule_test.go000066400000000000000000000032151256762333300204070ustar00rootroot00000000000000package inliner import "testing" // Reference: http://www.w3.org/TR/selectors/#specificity // // * /* a=0 b=0 c=0 -> specificity = 0 */ // LI /* a=0 b=0 c=1 -> specificity = 1 */ // UL LI /* a=0 b=0 c=2 -> specificity = 2 */ // UL OL+LI /* a=0 b=0 c=3 -> specificity = 3 */ // H1 + *[REL=up] /* a=0 b=1 c=1 -> specificity = 11 */ // UL OL LI.red /* a=0 b=1 c=3 -> specificity = 13 */ // LI.red.level /* a=0 b=2 c=1 -> specificity = 21 */ // #x34y /* a=1 b=0 c=0 -> specificity = 100 */ // #s12:not(FOO) /* a=1 b=0 c=1 -> specificity = 101 */ func TestComputeSpecificity(t *testing.T) { if val := ComputeSpecificity("*"); val != 0 { t.Fatal("Failed to compute specificity: ", val) } if val := ComputeSpecificity("LI"); val != 1 { t.Fatal("Failed to compute specificity: ", val) } if val := ComputeSpecificity("UL LI"); val != 2 { t.Fatal("Failed to compute specificity: ", val) } if val := ComputeSpecificity("UL OL+LI "); val != 3 { t.Fatal("Failed to compute specificity: ", val) } if val := ComputeSpecificity("H1 + *[REL=up]"); val != 11 { t.Fatal("Failed to compute specificity: ", val) } if val := ComputeSpecificity("UL OL LI.red"); val != 13 { t.Fatal("Failed to compute specificity: ", val) } if val := ComputeSpecificity("LI.red.level"); val != 21 { t.Fatal("Failed to compute specificity: ", val) } if val := ComputeSpecificity("#x34y"); val != 100 { t.Fatal("Failed to compute specificity: ", val) } // This one fails ! \o/ // if val := ComputeSpecificity("#s12:not(FOO)"); val != 101 { // t.Fatal("Failed to compute specificity: ", val) // } } douceur-0.2.0/parser/000077500000000000000000000000001256762333300144655ustar00rootroot00000000000000douceur-0.2.0/parser/parser.go000066400000000000000000000211421256762333300163100ustar00rootroot00000000000000package parser import ( "errors" "fmt" "regexp" "strings" "github.com/gorilla/css/scanner" "github.com/aymerick/douceur/css" ) const ( importantSuffixRegexp = `(?i)\s*!important\s*$` ) var ( importantRegexp *regexp.Regexp ) // Parser represents a CSS parser type Parser struct { scan *scanner.Scanner // Tokenizer // Tokens parsed but not consumed yet tokens []*scanner.Token // Rule embedding level embedLevel int } func init() { importantRegexp = regexp.MustCompile(importantSuffixRegexp) } // NewParser instanciates a new parser func NewParser(txt string) *Parser { return &Parser{ scan: scanner.New(txt), } } // Parse parses a whole stylesheet func Parse(text string) (*css.Stylesheet, error) { result, err := NewParser(text).ParseStylesheet() if err != nil { return nil, err } return result, nil } // ParseDeclarations parses CSS declarations func ParseDeclarations(text string) ([]*css.Declaration, error) { result, err := NewParser(text).ParseDeclarations() if err != nil { return nil, err } return result, nil } // ParseStylesheet parses a stylesheet func (parser *Parser) ParseStylesheet() (*css.Stylesheet, error) { result := css.NewStylesheet() // Parse BOM if _, err := parser.parseBOM(); err != nil { return result, err } // Parse list of rules rules, err := parser.ParseRules() if err != nil { return result, err } result.Rules = rules return result, nil } // ParseRules parses a list of rules func (parser *Parser) ParseRules() ([]*css.Rule, error) { result := []*css.Rule{} inBlock := false if parser.tokenChar("{") { // parsing a block of rules inBlock = true parser.embedLevel++ parser.shiftToken() } for parser.tokenParsable() { if parser.tokenIgnorable() { parser.shiftToken() } else if parser.tokenChar("}") { if !inBlock { errMsg := fmt.Sprintf("Unexpected } character: %s", parser.nextToken().String()) return result, errors.New(errMsg) } parser.shiftToken() parser.embedLevel-- // finished break } else { rule, err := parser.ParseRule() if err != nil { return result, err } rule.EmbedLevel = parser.embedLevel result = append(result, rule) } } return result, parser.err() } // ParseRule parses a rule func (parser *Parser) ParseRule() (*css.Rule, error) { if parser.tokenAtKeyword() { return parser.parseAtRule() } return parser.parseQualifiedRule() } // ParseDeclarations parses a list of declarations func (parser *Parser) ParseDeclarations() ([]*css.Declaration, error) { result := []*css.Declaration{} if parser.tokenChar("{") { parser.shiftToken() } for parser.tokenParsable() { if parser.tokenIgnorable() { parser.shiftToken() } else if parser.tokenChar("}") { // end of block parser.shiftToken() break } else { declaration, err := parser.ParseDeclaration() if err != nil { return result, err } result = append(result, declaration) } } return result, parser.err() } // ParseDeclaration parses a declaration func (parser *Parser) ParseDeclaration() (*css.Declaration, error) { result := css.NewDeclaration() curValue := "" for parser.tokenParsable() { if parser.tokenChar(":") { result.Property = strings.TrimSpace(curValue) curValue = "" parser.shiftToken() } else if parser.tokenChar(";") || parser.tokenChar("}") { if result.Property == "" { errMsg := fmt.Sprintf("Unexpected ; character: %s", parser.nextToken().String()) return result, errors.New(errMsg) } if importantRegexp.MatchString(curValue) { result.Important = true curValue = importantRegexp.ReplaceAllString(curValue, "") } result.Value = strings.TrimSpace(curValue) if parser.tokenChar(";") { parser.shiftToken() } // finished break } else { token := parser.shiftToken() curValue += token.Value } } // log.Printf("[parsed] Declaration: %s", result.String()) return result, parser.err() } // Parse an At Rule func (parser *Parser) parseAtRule() (*css.Rule, error) { // parse rule name (eg: "@import") token := parser.shiftToken() result := css.NewRule(css.AtRule) result.Name = token.Value for parser.tokenParsable() { if parser.tokenChar(";") { parser.shiftToken() // finished break } else if parser.tokenChar("{") { if result.EmbedsRules() { // parse rules block rules, err := parser.ParseRules() if err != nil { return result, err } result.Rules = rules } else { // parse declarations block declarations, err := parser.ParseDeclarations() if err != nil { return result, err } result.Declarations = declarations } // finished break } else { // parse prelude prelude, err := parser.parsePrelude() if err != nil { return result, err } result.Prelude = prelude } } // log.Printf("[parsed] Rule: %s", result.String()) return result, parser.err() } // Parse a Qualified Rule func (parser *Parser) parseQualifiedRule() (*css.Rule, error) { result := css.NewRule(css.QualifiedRule) for parser.tokenParsable() { if parser.tokenChar("{") { if result.Prelude == "" { errMsg := fmt.Sprintf("Unexpected { character: %s", parser.nextToken().String()) return result, errors.New(errMsg) } // parse declarations block declarations, err := parser.ParseDeclarations() if err != nil { return result, err } result.Declarations = declarations // finished break } else { // parse prelude prelude, err := parser.parsePrelude() if err != nil { return result, err } result.Prelude = prelude } } result.Selectors = strings.Split(result.Prelude, ",") for i, sel := range result.Selectors { result.Selectors[i] = strings.TrimSpace(sel) } // log.Printf("[parsed] Rule: %s", result.String()) return result, parser.err() } // Parse Rule prelude func (parser *Parser) parsePrelude() (string, error) { result := "" for parser.tokenParsable() && !parser.tokenEndOfPrelude() { token := parser.shiftToken() result += token.Value } result = strings.TrimSpace(result) // log.Printf("[parsed] prelude: %s", result) return result, parser.err() } // Parse BOM func (parser *Parser) parseBOM() (bool, error) { if parser.nextToken().Type == scanner.TokenBOM { parser.shiftToken() return true, nil } return false, parser.err() } // Returns next token without removing it from tokens buffer func (parser *Parser) nextToken() *scanner.Token { if len(parser.tokens) == 0 { // fetch next token nextToken := parser.scan.Next() // log.Printf("[token] %s => %v", nextToken.Type.String(), nextToken.Value) // queue it parser.tokens = append(parser.tokens, nextToken) } return parser.tokens[0] } // Returns next token and remove it from the tokens buffer func (parser *Parser) shiftToken() *scanner.Token { var result *scanner.Token result, parser.tokens = parser.tokens[0], parser.tokens[1:] return result } // Returns tokenizer error, or nil if no error func (parser *Parser) err() error { if parser.tokenError() { token := parser.nextToken() return fmt.Errorf("Tokenizer error: %s", token.String()) } return nil } // Returns true if next token is Error func (parser *Parser) tokenError() bool { return parser.nextToken().Type == scanner.TokenError } // Returns true if next token is EOF func (parser *Parser) tokenEOF() bool { return parser.nextToken().Type == scanner.TokenEOF } // Returns true if next token is a whitespace func (parser *Parser) tokenWS() bool { return parser.nextToken().Type == scanner.TokenS } // Returns true if next token is a comment func (parser *Parser) tokenComment() bool { return parser.nextToken().Type == scanner.TokenComment } // Returns true if next token is a CDO or a CDC func (parser *Parser) tokenCDOorCDC() bool { switch parser.nextToken().Type { case scanner.TokenCDO, scanner.TokenCDC: return true default: return false } } // Returns true if next token is ignorable func (parser *Parser) tokenIgnorable() bool { return parser.tokenWS() || parser.tokenComment() || parser.tokenCDOorCDC() } // Returns true if next token is parsable func (parser *Parser) tokenParsable() bool { return !parser.tokenEOF() && !parser.tokenError() } // Returns true if next token is an At Rule keyword func (parser *Parser) tokenAtKeyword() bool { return parser.nextToken().Type == scanner.TokenAtKeyword } // Returns true if next token is given character func (parser *Parser) tokenChar(value string) bool { token := parser.nextToken() return (token.Type == scanner.TokenChar) && (token.Value == value) } // Returns true if next token marks the end of a prelude func (parser *Parser) tokenEndOfPrelude() bool { return parser.tokenChar(";") || parser.tokenChar("{") } douceur-0.2.0/parser/parser_test.go000066400000000000000000000321671256762333300173600ustar00rootroot00000000000000package parser import ( "fmt" "strings" "testing" "github.com/aymerick/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("Failed to parse Qualified Rules", txt) } 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: []string{"p > a"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "color", Value: "blue", }, &css.Declaration{ Property: "text-decoration", Value: "underline", }, }, } 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: []string{"p > a"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "color", Value: "blue", Important: false, }, &css.Declaration{ Property: "text-decoration", Value: "underline", Important: true, }, &css.Declaration{ Property: "font-weight", Value: "normal", Important: true, }, }, } 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: []string{"table", "tr", "td"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "padding", Value: "0", }, }, } expectedRule2 := &css.Rule{ Kind: css.QualifiedRule, Prelude: `body, h1, h2, h3`, Selectors: []string{"body", "h1", "h2", "h3"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "color", Value: "#fff", }, }, } 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{ &css.Declaration{ Property: "system", Value: "symbolic", }, &css.Declaration{ Property: "symbols", Value: "'*' ⁑ † ‡", }, &css.Declaration{ Property: "suffix", Value: "''", }, }, } 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{ &css.Rule{ Kind: css.QualifiedRule, Prelude: "body", Selectors: []string{"body"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "color", Value: "purple", }, &css.Declaration{ Property: "background", Value: "yellow", }, }, }, }, } 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{ &css.Declaration{ Property: "font-family", Value: "MyHelvetica", }, &css.Declaration{ Property: "src", Value: `local("Helvetica Neue Bold"), local("HelveticaNeue-Bold"), url(MgOpenModernaBold.ttf)`, }, &css.Declaration{ Property: "font-weight", Value: "bold", }, }, } 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{ &css.Rule{ Kind: css.AtRule, Name: "@styleset", Declarations: []*css.Declaration{ &css.Declaration{ Property: "nice-style", Value: "4", }, }, }, }, } 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{ &css.Rule{ Kind: css.QualifiedRule, Prelude: "0%", Selectors: []string{"0%"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "top", Value: "0", }, &css.Declaration{ Property: "left", Value: "0", }, }, }, &css.Rule{ Kind: css.QualifiedRule, Prelude: "100%", Selectors: []string{"100%"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "top", Value: "100px", }, &css.Declaration{ Property: "left", Value: "100%", }, }, }, }, } 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{ &css.Rule{ Kind: css.QualifiedRule, Prelude: "body", Selectors: []string{"body"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "line-height", Value: "1.2", }, }, }, }, } 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{ &css.Declaration{ Property: "margin-left", Value: "4cm", }, &css.Declaration{ Property: "margin-right", Value: "3cm", }, }, } 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{ &css.Rule{ Kind: css.AtRule, Name: "@keyframes", Rules: []*css.Rule{ &css.Rule{ Kind: css.QualifiedRule, Prelude: "0%", Selectors: []string{"0%"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "top", Value: "0", }, &css.Declaration{ Property: "left", Value: "0", }, }, }, &css.Rule{ Kind: css.QualifiedRule, Prelude: "100%", Selectors: []string{"100%"}, Declarations: []*css.Declaration{ &css.Declaration{ Property: "top", Value: "100px", }, &css.Declaration{ Property: "left", Value: "100%", }, }, }, }, }, }, } 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{ &css.Declaration{ Property: "color", Value: "blue", }, &css.Declaration{ Property: "text-decoration", Value: "underline", }, } 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.String(), expectedOutput[i].String()) } } }