vendor dependencies with dep

This commit is contained in:
dhax 2017-09-25 20:20:52 +02:00
parent 93d8310491
commit 1384296a47
2712 changed files with 965742 additions and 0 deletions

3
vendor/github.com/vanng822/css/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
/src/
/pkg/
/bin/

16
vendor/github.com/vanng822/css/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,16 @@
language: golang
go:
- 1.4
env:
global:
- GOPATH="$HOME/gopath"
- PATH="$HOME/gopath/bin:$HOME/bin:$PATH"
install:
- go get github.com/gorilla/css/scanner
- go get github.com/stretchr/testify/assert
script:
- go test -v

23
vendor/github.com/vanng822/css/LICENSE generated vendored Normal file
View file

@ -0,0 +1,23 @@
Copyright (c) 2015 Nguyen Van Nhu
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.

23
vendor/github.com/vanng822/css/README.md generated vendored Normal file
View file

@ -0,0 +1,23 @@
# css
Package css is for parsing css stylesheet.
# Document
[![GoDoc](https://godoc.org/github.com/vanng822/css?status.svg)](https://godoc.org/github.com/vanng822/css)
# example
import (
"github.com/vanng822/css"
"fmt"
)
func main() {
csstext = "td {width: 100px; height: 100px;}"
ss := css.Parse(csstext)
rules := ss.GetCSSRuleList()
for _, rule := range rules {
fmt.Println(rule.Style.SelectorText)
fmt.Println(rule.Style.Styles)
}
}

140
vendor/github.com/vanng822/css/block_parser.go generated vendored Normal file
View file

@ -0,0 +1,140 @@
package css
import (
//"fmt"
"github.com/gorilla/css/scanner"
"strings"
)
type blockParserContext struct {
State State
NowProperty string
NowValue string
NowImportant int
}
// ParseBlock take a string of a css block,
// parses it and returns a map of css style declarations.
func ParseBlock(csstext string) map[string]*CSSStyleDeclaration {
s := scanner.New(csstext)
return parseBlock(s)
}
func parseBlock(s *scanner.Scanner) map[string]*CSSStyleDeclaration {
/* block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
property : IDENT;
value : [ any | block | ATKEYWORD S* ]+;
any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
| DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
| DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
| '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
] S*;
*/
decls := make(map[string]*CSSStyleDeclaration)
context := &blockParserContext{
State: STATE_NONE,
NowProperty: "",
NowValue: "",
NowImportant: 0,
}
for {
token := s.Next()
//fmt.Printf("BLOCK(%d): %s:'%s'\n", context.State, token.Type.String(), token.Value)
if token.Type == scanner.TokenError {
break
}
if token.Type == scanner.TokenEOF {
if context.State == STATE_VALUE {
// we are ending without ; or }
// this can happen when we parse only css declaration
decl := NewCSSStyleDeclaration(context.NowProperty, strings.TrimSpace(context.NowValue), context.NowImportant)
decls[context.NowProperty] = decl
}
break
}
switch token.Type {
case scanner.TokenS:
if context.State == STATE_VALUE {
context.NowValue += token.Value
}
case scanner.TokenIdent:
if context.State == STATE_NONE {
context.State = STATE_PROPERTY
context.NowProperty = strings.TrimSpace(token.Value)
break
}
if token.Value == "important" {
context.NowImportant = 1
} else {
context.NowValue += token.Value
}
case scanner.TokenChar:
if context.State == STATE_NONE {
if token.Value == "{" {
break
}
}
if context.State == STATE_PROPERTY {
if token.Value == ":" {
context.State = STATE_VALUE
}
// CHAR and STATE_PROPERTY but not : then weird
// break to ignore it
break
}
// should be no state or value
if token.Value == ";" {
decl := NewCSSStyleDeclaration(context.NowProperty, strings.TrimSpace(context.NowValue), context.NowImportant)
decls[context.NowProperty] = decl
context.NowProperty = ""
context.NowValue = ""
context.NowImportant = 0
context.State = STATE_NONE
} else if token.Value == "}" { // last property in a block can have optional ;
if context.State == STATE_VALUE {
// only valid if state is still VALUE, could be ;}
decl := NewCSSStyleDeclaration(context.NowProperty, strings.TrimSpace(context.NowValue), context.NowImportant)
decls[context.NowProperty] = decl
}
// we are done
return decls
} else if token.Value != "!" {
context.NowValue += token.Value
}
break
// any
case scanner.TokenNumber:
fallthrough
case scanner.TokenPercentage:
fallthrough
case scanner.TokenDimension:
fallthrough
case scanner.TokenString:
fallthrough
case scanner.TokenURI:
fallthrough
case scanner.TokenHash:
fallthrough
case scanner.TokenUnicodeRange:
fallthrough
case scanner.TokenIncludes:
fallthrough
case scanner.TokenDashMatch:
fallthrough
case scanner.TokenFunction:
fallthrough
case scanner.TokenSubstringMatch:
context.NowValue += token.Value
}
}
return decls
}

50
vendor/github.com/vanng822/css/block_parser_test.go generated vendored Normal file
View file

@ -0,0 +1,50 @@
package css
import (
"github.com/stretchr/testify/assert"
"testing"
//"fmt"
)
func TestParseBlock(t *testing.T) {
css := ParseBlock(`
font-family: "Source Sans Pro", Arial, sans-serif;
font-size: 27px;
line-height: 35px;`)
assert.Equal(t, len(css), 3)
assert.Equal(t, "35px", css["line-height"].Value)
}
func TestParseBlockOneLine(t *testing.T) {
css := ParseBlock("font-family: \"Source Sans Pro\", Arial, sans-serif; font-size: 27px;")
assert.Equal(t, len(css), 2)
assert.Equal(t, "27px", css["font-size"].Value)
assert.Equal(t, "\"Source Sans Pro\", Arial, sans-serif", css["font-family"].Value)
}
func TestParseBlockBlankEnd(t *testing.T) {
css := ParseBlock("font-size: 27px; width: 10px")
assert.Equal(t, len(css), 2)
assert.Equal(t, "27px", css["font-size"].Value)
assert.Equal(t, "10px", css["width"].Value)
}
func TestParseBlockInportant(t *testing.T) {
css := ParseBlock("font-size: 27px; width: 10px !important")
assert.Equal(t, len(css), 2)
assert.Equal(t, "27px", css["font-size"].Value)
assert.Equal(t, "10px", css["width"].Value)
assert.Equal(t, 1, css["width"].Important)
}
func TestParseBlockWithBraces(t *testing.T) {
css := ParseBlock("{ font-size: 27px; width: 10px }")
assert.Equal(t, len(css), 2)
assert.Equal(t, "27px", css["font-size"].Value)
assert.Equal(t, "10px", css["width"].Value)
}

51
vendor/github.com/vanng822/css/charset_parser.go generated vendored Normal file
View file

@ -0,0 +1,51 @@
package css
import (
//"fmt"
"github.com/gorilla/css/scanner"
"strings"
)
func newCharsetRule(statement string) *CSSRule {
statement = strings.TrimSpace(statement)
if statement != "" {
rule := NewRule(CHARSET_RULE)
rule.Style.SelectorText = statement
return rule
}
return nil
}
func parseCharset(s *scanner.Scanner) *CSSRule {
/*
Syntax:
@charset charset;
Example:
@charset "UTF-8";
*/
var statement string
for {
token := s.Next()
//fmt.Printf("Import: %s:'%s'\n", token.Type.String(), token.Value)
if token.Type == scanner.TokenEOF || token.Type == scanner.TokenError {
return nil
}
// take everything for now
switch token.Type {
case scanner.TokenChar:
if token.Value == ";" {
return newCharsetRule(statement)
}
statement += token.Value
default:
statement += token.Value
}
}
}

27
vendor/github.com/vanng822/css/charset_parser_test.go generated vendored Normal file
View file

@ -0,0 +1,27 @@
package css
import (
"github.com/gorilla/css/scanner"
"github.com/stretchr/testify/assert"
"testing"
)
func TestCharsetDoubleQ(t *testing.T) {
css := Parse(`@charset "UTF-8";`)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "\"UTF-8\"")
assert.Equal(t, css.CssRuleList[0].Type, CHARSET_RULE)
}
func TestCharsetSingleQ(t *testing.T) {
css := Parse(`@charset 'iso-8859-15';`)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "'iso-8859-15'")
assert.Equal(t, css.CssRuleList[0].Type, CHARSET_RULE)
}
func TestCharsetIgnore(t *testing.T) {
css := parseCharset(scanner.New(` 'iso-8859-15'`))
assert.Nil(t, css)
}

16
vendor/github.com/vanng822/css/doc.go generated vendored Normal file
View file

@ -0,0 +1,16 @@
// Package css is for parsing css stylesheet.
//
// import (
// "github.com/vanng822/css"
// "fmt"
// )
// func main() {
// csstext = "td {width: 100px; height: 100px;}"
// ss := css.Parse(csstext)
// rules := ss.GetCSSRuleList()
// for _, rule := range rules {
// fmt.Println(rule.Style.SelectorText)
// fmt.Println(rule.Style.Styles)
// }
// }
package css

56
vendor/github.com/vanng822/css/import_parser.go generated vendored Normal file
View file

@ -0,0 +1,56 @@
package css
import (
//"fmt"
"github.com/gorilla/css/scanner"
"strings"
)
func newImportRule(statement string) *CSSRule {
statement = strings.TrimSpace(statement)
if statement != "" {
rule := NewRule(IMPORT_RULE)
rule.Style.SelectorText = statement
return rule
}
return nil
}
func parseImport(s *scanner.Scanner) *CSSRule {
/*
Syntax:
@import url; or
@import url list-of-media-queries;
Example:
@import url("fineprint.css") print;
@import url("bluish.css") projection, tv;
@import 'custom.css';
@import url("chrome://communicator/skin/");
@import "common.css" screen, projection;
@import url('landscape.css') screen and (orientation:landscape);
*/
var statement string
for {
token := s.Next()
//fmt.Printf("Import: %s:'%s'\n", token.Type.String(), token.Value)
if token.Type == scanner.TokenEOF || token.Type == scanner.TokenError {
return nil
}
// take everything for now
switch token.Type {
case scanner.TokenChar:
if token.Value == ";" {
return newImportRule(statement)
}
statement += token.Value
default:
statement += token.Value
}
}
}

35
vendor/github.com/vanng822/css/import_parser_test.go generated vendored Normal file
View file

@ -0,0 +1,35 @@
package css
import (
"github.com/gorilla/css/scanner"
"github.com/stretchr/testify/assert"
"testing"
)
func TestImport(t *testing.T) {
css := Parse(`@import url("fineprint.css") print;
@import url("bluish.css") projection, tv;
@import 'custom.css';
@import url("chrome://communicator/skin/");
@import "common.css" screen, projection;
@import url('landscape.css') screen and (orientation:landscape);`)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "url(\"fineprint.css\") print")
assert.Equal(t, css.CssRuleList[1].Style.SelectorText, "url(\"bluish.css\") projection, tv")
assert.Equal(t, css.CssRuleList[2].Style.SelectorText, "'custom.css'")
assert.Equal(t, css.CssRuleList[3].Style.SelectorText, "url(\"chrome://communicator/skin/\")")
assert.Equal(t, css.CssRuleList[4].Style.SelectorText, "\"common.css\" screen, projection")
assert.Equal(t, css.CssRuleList[5].Style.SelectorText, "url('landscape.css') screen and (orientation:landscape)")
assert.Equal(t, css.CssRuleList[0].Type, IMPORT_RULE)
assert.Equal(t, css.CssRuleList[1].Type, IMPORT_RULE)
assert.Equal(t, css.CssRuleList[2].Type, IMPORT_RULE)
assert.Equal(t, css.CssRuleList[3].Type, IMPORT_RULE)
assert.Equal(t, css.CssRuleList[4].Type, IMPORT_RULE)
assert.Equal(t, css.CssRuleList[5].Type, IMPORT_RULE)
}
func TestImportIgnore(t *testing.T) {
css := parseImport(scanner.New(` url("fineprint.css") print`))
assert.Nil(t, css)
}

154
vendor/github.com/vanng822/css/parser.go generated vendored Normal file
View file

@ -0,0 +1,154 @@
package css
import (
"fmt"
"github.com/gorilla/css/scanner"
"strings"
)
/*
stylesheet : [ CDO | CDC | S | statement ]*;
statement : ruleset | at-rule;
at-rule : ATKEYWORD S* any* [ block | ';' S* ];
block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*;
selector : any+;
declaration : property S* ':' S* value;
property : IDENT;
value : [ any | block | ATKEYWORD S* ]+;
any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
| DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
| DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
| '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
] S*;
unused : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*;
*/
type State int
const (
STATE_NONE State = iota
STATE_SELECTOR
STATE_PROPERTY
STATE_VALUE
)
type parserContext struct {
State State
NowSelectorText string
NowRuleType RuleType
CurrentRule *CSSRule
CurrentMediaRule *CSSRule
}
func resetContextStyleRule(context *parserContext) {
context.CurrentRule = nil
context.NowSelectorText = ""
context.NowRuleType = STYLE_RULE
context.State = STATE_NONE
}
func parseRule(context *parserContext, s *scanner.Scanner, css *CSSStyleSheet) {
context.CurrentRule = NewRule(context.NowRuleType)
context.NowSelectorText += parseSelector(s)
context.CurrentRule.Style.SelectorText = strings.TrimSpace(context.NowSelectorText)
context.CurrentRule.Style.Styles = parseBlock(s)
if context.CurrentMediaRule != nil {
context.CurrentMediaRule.Rules = append(context.CurrentMediaRule.Rules, context.CurrentRule)
} else {
css.CssRuleList = append(css.CssRuleList, context.CurrentRule)
}
}
// Parse takes a string of valid css rules, stylesheet,
// and parses it. Be aware this function has poor error handling
// so you should have valid syntax in your css
func Parse(csstext string) *CSSStyleSheet {
context := &parserContext{
State: STATE_NONE,
NowSelectorText: "",
NowRuleType: STYLE_RULE,
CurrentMediaRule: nil,
}
css := &CSSStyleSheet{}
css.CssRuleList = make([]*CSSRule, 0)
s := scanner.New(csstext)
for {
token := s.Next()
//fmt.Printf("Parse(%d): %s:'%s'\n", context.State, token.Type.String(), token.Value)
if token.Type == scanner.TokenEOF || token.Type == scanner.TokenError {
break
}
switch token.Type {
case scanner.TokenCDO:
break
case scanner.TokenCDC:
break
case scanner.TokenComment:
break
case scanner.TokenS:
break
case scanner.TokenAtKeyword:
switch token.Value {
case "@media":
context.NowRuleType = MEDIA_RULE
case "@font-face":
// Parse as normal rule, would be nice to parse according to syntax
// https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face
context.NowRuleType = FONT_FACE_RULE
parseRule(context, s, css)
resetContextStyleRule(context)
case "@import":
// No validation
// https://developer.mozilla.org/en-US/docs/Web/CSS/@import
rule := parseImport(s)
if rule != nil {
css.CssRuleList = append(css.CssRuleList, rule)
}
resetContextStyleRule(context)
case "@charset":
// No validation
// https://developer.mozilla.org/en-US/docs/Web/CSS/@charset
rule := parseCharset(s)
if rule != nil {
css.CssRuleList = append(css.CssRuleList, rule)
}
resetContextStyleRule(context)
case "@page":
context.NowRuleType = PAGE_RULE
parseRule(context, s, css)
resetContextStyleRule(context)
default:
panic(fmt.Sprintf("At rule '%s' is not supported", token.Value))
}
default:
if context.State == STATE_NONE {
if token.Value == "}" && context.CurrentMediaRule != nil {
// close media rule
css.CssRuleList = append(css.CssRuleList, context.CurrentMediaRule)
context.CurrentMediaRule = nil
break
}
}
if context.NowRuleType == MEDIA_RULE {
context.CurrentMediaRule = NewRule(context.NowRuleType)
context.CurrentMediaRule.Style.SelectorText = strings.TrimSpace(token.Value + parseSelector(s))
resetContextStyleRule(context)
break
} else {
context.NowSelectorText += token.Value
parseRule(context, s, css)
resetContextStyleRule(context)
break
}
}
}
return css
}

103
vendor/github.com/vanng822/css/parser_media_test.go generated vendored Normal file
View file

@ -0,0 +1,103 @@
package css
import (
//"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestMedia(t *testing.T) {
css := Parse(`@media only screen and (max-width: 600px) {
table[class="body"] img {
width: auto !important;
height: auto !important
}
table[class="body"] center {
min-width: 0 !important
}
table[class="body"] .container {
width: 95% !important
}
table[class="body"] .row {
width: 100% !important;
display: block !important
}
}`)
//fmt.Println(css)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "only screen and (max-width: 600px)")
assert.Equal(t, css.CssRuleList[0].Type, MEDIA_RULE)
assert.Equal(t, len(css.CssRuleList[0].Rules), 4)
assert.Equal(t, css.CssRuleList[0].Rules[0].Style.SelectorText, "table[class=\"body\"] img")
assert.Equal(t, css.CssRuleList[0].Rules[0].Style.Styles["height"].Value, "auto")
assert.Equal(t, css.CssRuleList[0].Rules[0].Style.Styles["height"].Important, 1)
assert.Equal(t, css.CssRuleList[0].Rules[1].Style.SelectorText, "table[class=\"body\"] center")
assert.Equal(t, css.CssRuleList[0].Rules[2].Style.SelectorText, "table[class=\"body\"] .container")
assert.Equal(t, css.CssRuleList[0].Rules[3].Style.SelectorText, "table[class=\"body\"] .row")
}
func TestMediaMulti(t *testing.T) {
css := Parse(`
table.one {
width: 30px;
}
@media only screen and (max-width: 600px) {
table[class="body"] img {
width: auto !important;
height: auto !important
}
table[class="body"] center {
min-width: 0 !important
}
table[class="body"] .container {
width: 95% !important
}
table[class="body"] .row {
width: 100% !important;
display: block !important
}
}
@media all and (min-width: 48em) {
blockquote {
font-size: 34px;
line-height: 40px;
padding-top: 2px;
padding-bottom: 3px;
}
}
table.two {
width: 80px;
}`)
assert.Equal(t, len(css.CssRuleList), 4)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "table.one")
assert.Equal(t, css.CssRuleList[0].Type, STYLE_RULE)
assert.Equal(t, css.CssRuleList[0].Style.Styles["width"].Value, "30px")
assert.Equal(t, len(css.CssRuleList[0].Rules), 0)
assert.Equal(t, css.CssRuleList[1].Style.SelectorText, "only screen and (max-width: 600px)")
assert.Equal(t, css.CssRuleList[1].Type, MEDIA_RULE)
assert.Equal(t, len(css.CssRuleList[1].Rules), 4)
assert.Equal(t, css.CssRuleList[1].Rules[0].Style.SelectorText, "table[class=\"body\"] img")
assert.Equal(t, css.CssRuleList[1].Rules[0].Style.Styles["height"].Value, "auto")
assert.Equal(t, css.CssRuleList[1].Rules[0].Style.Styles["height"].Important, 1)
assert.Equal(t, css.CssRuleList[1].Rules[1].Style.SelectorText, "table[class=\"body\"] center")
assert.Equal(t, css.CssRuleList[1].Rules[2].Style.SelectorText, "table[class=\"body\"] .container")
assert.Equal(t, css.CssRuleList[1].Rules[3].Style.SelectorText, "table[class=\"body\"] .row")
assert.Equal(t, css.CssRuleList[2].Style.SelectorText, "all and (min-width: 48em)")
assert.Equal(t, css.CssRuleList[2].Type, MEDIA_RULE)
assert.Equal(t, css.CssRuleList[2].Rules[0].Style.SelectorText, "blockquote")
assert.Equal(t, css.CssRuleList[2].Rules[0].Style.Styles["font-size"].Value, "34px")
assert.Equal(t, css.CssRuleList[2].Rules[0].Style.Styles["line-height"].Value, "40px")
assert.Equal(t, css.CssRuleList[2].Rules[0].Style.Styles["padding-top"].Value, "2px")
assert.Equal(t, css.CssRuleList[2].Rules[0].Style.Styles["padding-bottom"].Value, "3px")
assert.Equal(t, len(css.CssRuleList[2].Rules), 1)
assert.Equal(t, css.CssRuleList[3].Style.SelectorText, "table.two")
assert.Equal(t, css.CssRuleList[3].Type, STYLE_RULE)
assert.Equal(t, css.CssRuleList[3].Style.Styles["width"].Value, "80px")
assert.Equal(t, len(css.CssRuleList[3].Rules), 0)
}

175
vendor/github.com/vanng822/css/parser_selector_test.go generated vendored Normal file
View file

@ -0,0 +1,175 @@
package css
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestMultipleSelectors(t *testing.T) {
css := Parse(`div .a {
font-size: 150%;
}
p .b {
font-size: 250%;
}`)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "div .a")
assert.Equal(t, css.CssRuleList[1].Style.SelectorText, "p .b")
}
func TestIdSelector(t *testing.T) {
css := Parse("#div { color: red;}")
assert.Equal(t, css.CssRuleList[0].Style.Styles["color"].Value, "red")
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "#div")
}
func TestClassSelector(t *testing.T) {
css := Parse(".div { color: green;}")
assert.Equal(t, css.CssRuleList[0].Style.Styles["color"].Value, "green")
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, ".div")
}
func TestStarSelector(t *testing.T) {
css := Parse("* { text-rendering: optimizelegibility; }")
assert.Equal(t, "optimizelegibility", css.CssRuleList[0].Style.Styles["text-rendering"].Value)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "*")
}
func TestStarSelectorMulti(t *testing.T) {
css := Parse(`div .a {
font-size: 150%;
}
* { text-rendering: optimizelegibility; }`)
assert.Equal(t, "150%", css.CssRuleList[0].Style.Styles["font-size"].Value)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "div .a")
assert.Equal(t, "optimizelegibility", css.CssRuleList[1].Style.Styles["text-rendering"].Value)
assert.Equal(t, css.CssRuleList[1].Style.SelectorText, "*")
}
func TestMixedClassSelectors(t *testing.T) {
selectors := []string{".footer__content_wrapper--last",
"table[class=\"body\"] .footer__content td",
"table[class=\"body\"] td.footer__link_wrapper--first",
"table[class=\"body\"] td.footer__link_wrapper--last"}
for _, selector := range selectors {
css := Parse(fmt.Sprintf(` %s {
border-collapse: separate;
padding: 10px 0 0
}`, selector))
assert.Equal(t, "separate", css.CssRuleList[0].Style.Styles["border-collapse"].Value)
assert.Equal(t, "10px 0 0", css.CssRuleList[0].Style.Styles["padding"].Value)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, selector)
}
}
func TestGenericSelectors(t *testing.T) {
selectors := []string{
"p ~ ul",
"div > p",
"div > p",
"div p",
"div, p",
"[target]",
"[target=_blank]",
"[title~=flower]",
"[lang|=en]",
"a[href^=\"https\"]",
"a[href$=\".pdf\"]",
"a[href*=\"css\"]",
".header + .content",
"#firstname",
"table[class=\"body\"] .footer__content td",
"table[class=\"body\"] td.footer__link_wrapper--first"}
for _, selector := range selectors {
css := Parse(fmt.Sprintf(` %s {
border-collapse: separate;
padding: 10px 0 0
}`, selector))
assert.Equal(t, "separate", css.CssRuleList[0].Style.Styles["border-collapse"].Value)
assert.Equal(t, "10px 0 0", css.CssRuleList[0].Style.Styles["padding"].Value)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, selector)
}
}
func TestFilterSelectors(t *testing.T) {
selectors := []string{
"a:active",
"p::after",
"p::before",
"input:checked",
"input:disabled",
"input:in-range",
"input:invalid",
"input:optional",
"input:read-only",
"input:enabled",
"p:empty",
"p:first-child",
"p::first-letter",
"p::first-line",
"p:first-of-type",
"input:focus",
"a:hover",
"p:lang(it)",
"p:last-child",
"p:last-of-type",
"a:link",
":not(p)",
"p:nth-child(2)",
"p:nth-last-child(2)",
"p:only-of-type",
"p:only-child",
"p:nth-last-of-type(2)",
"div:not(:nth-child(1))",
"div:not(:not(:first-child))",
":root",
"::selection",
"#news:target"}
for _, selector := range selectors {
css := Parse(fmt.Sprintf(` %s {
border-collapse: separate;
padding: 10px 0 0
}`, selector))
assert.Equal(t, "separate", css.CssRuleList[0].Style.Styles["border-collapse"].Value)
assert.Equal(t, "10px 0 0", css.CssRuleList[0].Style.Styles["padding"].Value)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, selector)
}
}
func TestFontFace(t *testing.T) {
css := Parse(`@font-face {
font-family: "Bitstream Vera Serif Bold";
src: url("https://mdn.mozillademos.org/files/2468/VeraSeBd.ttf");
}
body { font-family: "Bitstream Vera Serif Bold", serif }`)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-family"].Value, "\"Bitstream Vera Serif Bold\"")
assert.Equal(t, css.CssRuleList[0].Style.Styles["src"].Value, "url(\"https://mdn.mozillademos.org/files/2468/VeraSeBd.ttf\")")
assert.Equal(t, css.CssRuleList[1].Style.Styles["font-family"].Value, "\"Bitstream Vera Serif Bold\", serif")
assert.Equal(t, css.CssRuleList[0].Type, FONT_FACE_RULE)
}
func TestPage(t *testing.T) {
css := Parse(`@page :first {
margin: 2in 3in;
}`)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, ":first")
assert.Equal(t, css.CssRuleList[0].Style.Styles["margin"].Value, "2in 3in")
assert.Equal(t, css.CssRuleList[0].Type, PAGE_RULE)
}

148
vendor/github.com/vanng822/css/parser_test.go generated vendored Normal file
View file

@ -0,0 +1,148 @@
package css
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithoutImpotant(t *testing.T) {
css := Parse(`div .a { font-size: 150%;}`)
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Value, "150%")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Property, "font-size")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Important, 0)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "div .a")
}
func TestWithImpotant(t *testing.T) {
css := Parse("div .a { font-size: 150% !important;}")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Value, "150%")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Property, "font-size")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Important, 1)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "div .a")
}
func TestMultipleDeclarations(t *testing.T) {
css := Parse(`div .a {
font-size: 150%;
width: 100%
}`)
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Value, "150%")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Property, "font-size")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Important, 0)
assert.Equal(t, css.CssRuleList[0].Style.Styles["width"].Value, "100%")
assert.Equal(t, css.CssRuleList[0].Style.Styles["width"].Property, "width")
assert.Equal(t, css.CssRuleList[0].Style.Styles["width"].Important, 0)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "div .a")
}
func TestValuePx(t *testing.T) {
css := Parse("div .a { font-size: 45px;}")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Value, "45px")
}
func TestValueEm(t *testing.T) {
css := Parse("div .a { font-size: 45em;}")
assert.Equal(t, css.CssRuleList[0].Style.Styles["font-size"].Value, "45em")
}
func TestValueHex(t *testing.T) {
css := Parse("div .a { color: #123456;}")
assert.Equal(t, css.CssRuleList[0].Style.Styles["color"].Value, "#123456")
}
func TestValueRGBFunction(t *testing.T) {
css := Parse(".color{ color: rgb(1,2,3);}")
assert.Equal(t, css.CssRuleList[0].Style.Styles["color"].Value, "rgb(1,2,3)")
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, ".color")
}
func TestValueString(t *testing.T) {
css := Parse("div .center { text-align: center; }")
assert.Equal(t, css.CssRuleList[0].Style.Styles["text-align"].Value, "center")
}
func TestValueWhiteSpace(t *testing.T) {
css := Parse(".div { padding: 10px 0 0 10px}")
assert.Equal(t, "10px 0 0 10px", css.CssRuleList[0].Style.Styles["padding"].Value)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, ".div")
}
func TestValueMixed(t *testing.T) {
css := Parse(`td {
padding: 0 12px 0 10px;
border-right: 1px solid white
}`)
assert.Equal(t, "0 12px 0 10px", css.CssRuleList[0].Style.Styles["padding"].Value)
assert.Equal(t, "1px solid white", css.CssRuleList[0].Style.Styles["border-right"].Value)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "td")
}
func TestQuoteValue(t *testing.T) {
css := Parse(`blockquote {
font-family: "Source Sans Pro", Arial, sans-serif;
font-size: 27px;
line-height: 35px;}`)
assert.Equal(t, "\"Source Sans Pro\", Arial, sans-serif", css.CssRuleList[0].Style.Styles["font-family"].Value)
assert.Equal(t, "27px", css.CssRuleList[0].Style.Styles["font-size"].Value)
assert.Equal(t, "35px", css.CssRuleList[0].Style.Styles["line-height"].Value)
assert.Equal(t, css.CssRuleList[0].Style.SelectorText, "blockquote")
}
func TestDashClassname(t *testing.T) {
css := Parse(`.content {
padding: 0px;
}
.content-wrap {
padding: 2px;
}`)
assert.Equal(t, ".content", css.CssRuleList[0].Style.SelectorText)
assert.Equal(t, ".content-wrap", css.CssRuleList[1].Style.SelectorText)
assert.Equal(t, "0px", css.CssRuleList[0].Style.Styles["padding"].Value)
assert.Equal(t, "2px", css.CssRuleList[1].Style.Styles["padding"].Value)
}
func TestNotSupportedAtRule(t *testing.T) {
rules := []string{
`@keyframes mymove {
0% {top: 0px;}
25% {top: 200px;}
50% {top: 100px;}
75% {top: 200px;}
100% {top: 0px;}
}`,
`@-webkit-keyframes mymove {
0% {top: 0px;}
25% {top: 200px;}
50% {top: 100px;}
75% {top: 200px;}
100% {top: 0px;}
} `,
`@counter-style winners-list {
system: fixed;
symbols: url(gold-medal.svg) url(silver-medal.svg) url(bronze-medal.svg);
suffix: " ";
}`,
`@namespace url(http://www.w3.org/1999/xhtml);`,
`@document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*")
{
body { color: purple; background: yellow; }
}`,
}
for _, rule := range rules {
assert.Panics(t, func() {
Parse(rule)
})
}
}

43
vendor/github.com/vanng822/css/rule.go generated vendored Normal file
View file

@ -0,0 +1,43 @@
package css
import ()
type RuleType int
const (
STYLE_RULE RuleType = iota
CHARSET_RULE
IMPORT_RULE
MEDIA_RULE
FONT_FACE_RULE
PAGE_RULE
)
var ruleTypeNames = map[RuleType]string{
STYLE_RULE: "",
MEDIA_RULE: "@media",
CHARSET_RULE: "@charset",
IMPORT_RULE: "@import",
FONT_FACE_RULE: "@font-face",
PAGE_RULE: "@page",
}
func (rt RuleType) Text() string {
return ruleTypeNames[rt]
}
type CSSRule struct {
Type RuleType
Style CSSStyleRule
Rules []*CSSRule
}
func NewRule(ruleType RuleType) *CSSRule {
r := &CSSRule{
Type: ruleType,
}
r.Style.Styles = make(map[string]*CSSStyleDeclaration)
r.Rules = make([]*CSSRule, 0)
return r
}

16
vendor/github.com/vanng822/css/rule_test.go generated vendored Normal file
View file

@ -0,0 +1,16 @@
package css
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestRuleTypeString(t *testing.T) {
assert.Equal(t, STYLE_RULE.Text(), "")
assert.Equal(t, CHARSET_RULE.Text(), "@charset")
assert.Equal(t, IMPORT_RULE.Text(), "@import")
assert.Equal(t, MEDIA_RULE.Text(), "@media")
assert.Equal(t, FONT_FACE_RULE.Text(), "@font-face")
assert.Equal(t, PAGE_RULE.Text(), "@page")
}

69
vendor/github.com/vanng822/css/selector_parser.go generated vendored Normal file
View file

@ -0,0 +1,69 @@
package css
import (
//"fmt"
"github.com/gorilla/css/scanner"
)
func parseSelector(s *scanner.Scanner) string {
/*
selector : any+;
any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
| DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
| DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
| '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
] S*;
*/
selector := ""
for {
token := s.Next()
//fmt.Printf("SELECTOR: %s:'%s'\n", token.Type.String(), token.Value)
if token.Type == scanner.TokenError || token.Type == scanner.TokenEOF {
break
}
switch token.Type {
case scanner.TokenChar:
if token.Value == "{" {
return selector
}
fallthrough
case scanner.TokenIdent:
fallthrough
case scanner.TokenS:
fallthrough
case scanner.TokenNumber:
fallthrough
case scanner.TokenPercentage:
fallthrough
case scanner.TokenDimension:
fallthrough
case scanner.TokenString:
fallthrough
case scanner.TokenURI:
fallthrough
case scanner.TokenHash:
fallthrough
case scanner.TokenUnicodeRange:
fallthrough
case scanner.TokenIncludes:
fallthrough
case scanner.TokenDashMatch:
fallthrough
case scanner.TokenFunction:
fallthrough
case scanner.TokenSuffixMatch:
fallthrough
case scanner.TokenPrefixMatch:
fallthrough
case scanner.TokenSubstringMatch:
selector += token.Value
}
}
return selector
}

26
vendor/github.com/vanng822/css/styledeclaration.go generated vendored Normal file
View file

@ -0,0 +1,26 @@
package css
import (
"fmt"
)
type CSSStyleDeclaration struct {
Property string
Value string
Important int
}
func NewCSSStyleDeclaration(property, value string, important int) *CSSStyleDeclaration {
return &CSSStyleDeclaration{
Property: property,
Value: value,
Important: important,
}
}
func (decl *CSSStyleDeclaration) Text() string {
if decl.Important == 1 {
return fmt.Sprintf("%s: %s !important", decl.Property, decl.Value)
}
return fmt.Sprintf("%s: %s", decl.Property, decl.Value)
}

View file

@ -0,0 +1,16 @@
package css
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestDeclWithImportan(t *testing.T) {
decl := NewCSSStyleDeclaration("width", "100%", 1)
assert.Equal(t, decl.Text(), "width: 100% !important")
}
func TestDeclWithoutImportan(t *testing.T) {
decl := NewCSSStyleDeclaration("width", "100%", 0)
assert.Equal(t, decl.Text(), "width: 100%")
}

24
vendor/github.com/vanng822/css/stylerule.go generated vendored Normal file
View file

@ -0,0 +1,24 @@
package css
import (
"fmt"
"sort"
"strings"
)
type CSSStyleRule struct {
SelectorText string
Styles map[string]*CSSStyleDeclaration
}
func (sr *CSSStyleRule) Text() string {
decls := make([]string, 0, len(sr.Styles))
for _, s := range sr.Styles {
decls = append(decls, s.Text())
}
sort.Strings(decls)
return fmt.Sprintf("%s {\n%s\n}", sr.SelectorText, strings.Join(decls, ";\n"))
}

16
vendor/github.com/vanng822/css/stylerule_test.go generated vendored Normal file
View file

@ -0,0 +1,16 @@
package css
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestStyleRuleText(t *testing.T) {
sr := CSSStyleRule{}
sr.SelectorText = ".box"
sr.Styles = make(map[string]*CSSStyleDeclaration)
sr.Styles["width"] = NewCSSStyleDeclaration("width", "10px", 0)
sr.Styles["height"] = NewCSSStyleDeclaration("height", "100px", 0)
assert.Equal(t, sr.Text(), ".box {\nheight: 100px;\nwidth: 10px\n}")
}

13
vendor/github.com/vanng822/css/stylesheet.go generated vendored Normal file
View file

@ -0,0 +1,13 @@
package css
import ()
type CSSStyleSheet struct {
Type string
Media string
CssRuleList []*CSSRule
}
func (ss *CSSStyleSheet) GetCSSRuleList() []*CSSRule {
return ss.CssRuleList
}

3
vendor/github.com/vanng822/go-premailer/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
/pkg/
.project
/src/

22
vendor/github.com/vanng822/go-premailer/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,22 @@
language: golang
go:
- 1.5
- 1.6
- 1.7
- 1.8
- tip
env:
global:
- GOPATH="$HOME/gopath"
- PATH="$HOME/gopath/bin:$HOME/bin:$PATH"
install:
- go get github.com/vanng822/css
- go get github.com/PuerkitoBio/goquery
- go get golang.org/x/net/html
- go get github.com/stretchr/testify/assert
script:
- cd premailer && go test -v

23
vendor/github.com/vanng822/go-premailer/LICENSE generated vendored Normal file
View file

@ -0,0 +1,23 @@
Copyright (c) 2015 Nguyen Van Nhu
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.

80
vendor/github.com/vanng822/go-premailer/README.md generated vendored Normal file
View file

@ -0,0 +1,80 @@
# go-premailer
Inline styling for html in golang
# Document
[![GoDoc](https://godoc.org/github.com/vanng822/go-premailer/premailer?status.svg)](https://godoc.org/github.com/vanng822/go-premailer/premailer)
# install
go get github.com/vanng822/go-premailer/premailer
# Example
import (
"fmt"
"github.com/vanng822/go-premailer/premailer"
"log"
)
func main() {
prem := premailer.NewPremailerFromFile(inputFile, premailer.NewOptions())
html, err := prem.Transform()
if err != nil {
log.Fatal(err)
}
fmt.Println(html)
}
## Input
<html>
<head>
<title>Title</title>
<style type="text/css">
h1 { width: 300px; color:red; }
strong { text-decoration:none; }
</style>
</head>
<body>
<h1>Hi!</h1>
<p><strong>Yes!</strong></p>
</body>
</html>
## Output
<html>
<head>
<title>Title</title>
</head>
<body>
<h1 style="color:red;width:300px" width="300">Hi!</h1>
<p><strong style="text-decoration:none">Yes!</strong></p>
</body>
</html>
# Commandline
> go run main.go -i your_email.html
> go run main.go -i your_mail.html -o process_mail.html
# Demo
http://premailer.isgoodness.com/
# Conversion endpoint
http://premailer.isgoodness.com/convert
request POST:
html: your mail
cssToAttributes: true|false
removeClasses: true|false
response:
{result: output}

48
vendor/github.com/vanng822/go-premailer/cmd/main.go generated vendored Normal file
View file

@ -0,0 +1,48 @@
package main
import (
"flag"
"fmt"
"github.com/vanng822/go-premailer/premailer"
"log"
"os"
"time"
)
func main() {
var (
inputFile string
outputFile string
removeClasses bool
skipCssToAttributes bool
)
flag.StringVar(&inputFile, "i", "", "Input file")
flag.StringVar(&outputFile, "o", "", "Output file")
flag.BoolVar(&removeClasses, "remove-classes", false, "Remove class attribute")
flag.BoolVar(&skipCssToAttributes, "skip-css-to-attributes", false, "No copy of css property to html attribute")
flag.Parse()
if inputFile == "" {
flag.Usage()
return
}
start := time.Now()
options := premailer.NewOptions()
options.RemoveClasses = removeClasses
options.CssToAttributes = !skipCssToAttributes
prem := premailer.NewPremailerFromFile(inputFile, options)
html, err := prem.Transform()
log.Printf("took: %v", time.Now().Sub(start))
if err != nil {
log.Fatal(err)
}
if outputFile != "" {
fd, err := os.Create(outputFile)
if err != nil {
log.Fatal(err)
}
defer fd.Close()
fd.WriteString(html)
} else {
fmt.Println(html)
}
}

View file

@ -0,0 +1,62 @@
package main
import (
"flag"
"fmt"
"github.com/unrolled/render"
"github.com/vanng822/go-premailer/premailer"
"github.com/vanng822/r2router"
"log"
"net/http"
"os"
"os/signal"
"syscall"
)
func main() {
var (
host string
port int
)
flag.StringVar(&host, "h", "127.0.0.1", "Host to listen on")
flag.IntVar(&port, "p", 8080, "Port number to listen on")
flag.Parse()
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Kill, os.Interrupt, syscall.SIGTERM, syscall.SIGUSR2)
app := r2router.NewSeeforRouter()
r := render.New()
app.Get("/", func(w http.ResponseWriter, req *http.Request, _ r2router.Params) {
r.JSON(w, http.StatusOK, r2router.M{"usage": "POST / html=HTML&cssToAttributes=boolean&removeClasses=boolean"})
})
app.Post("/", func(w http.ResponseWriter, req *http.Request, _ r2router.Params) {
req.ParseForm()
html := req.Form.Get("html")
cssToAttributes := req.Form.Get("cssToAttributes")
removeClasses := req.Form.Get("removeClasses")
var result string
if html != "" {
options := premailer.NewOptions()
if removeClasses == "true" {
options.RemoveClasses = true
}
if cssToAttributes == "false" {
options.CssToAttributes = false
}
pre := premailer.NewPremailerFromString(html, options)
result, _ = pre.Transform()
} else {
result = ""
}
r.JSON(w, http.StatusOK, r2router.M{"result": result})
})
log.Printf("listening to address %s:%d", host, port)
go http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), app)
sig := <-sigc
log.Printf("Got signal: %s", sig)
}

View file

@ -0,0 +1,30 @@
<html>
<head>
<title>Title</title>
<style type="text/css">
h1 {
width: 50px;
color:red;
}
h2 {
vertical-align: top;
}
h3 {
text-align: right;
}
strong {
text-decoration:none
}
div {
background-color: green
}
</style>
</head>
<body>
<h1>Hi!</h1>
<h2>There</h2>
<h3>Hello</h3>
<p><strong>Yes!</strong></p>
<div>Green color</div>
</body>
</html>

View file

@ -0,0 +1,45 @@
// Package premailer is for inline styling.
//
// import (
// "fmt"
// "github.com/vanng822/go-premailer/premailer"
// "log"
// )
//
// func main() {
// prem := premailer.NewPremailerFromFile(inputFile, premailer.NewOptions())
// html, err := prem.Transform()
// if err != nil {
// log.Fatal(err)
// }
//
// fmt.Println(html)
// }
// // Input
//
// <html>
// <head>
// <title>Title</title>
// <style type="text/css">
// h1 { width: 300px; color:red; }
// strong { text-decoration:none; }
// </style>
// </head>
// <body>
// <h1>Hi!</h1>
// <p><strong>Yes!</strong></p>
// </body>
// </html>
//
// // Output
//
// <html>
// <head>
// <title>Title</title>
// </head>
// <body>
// <h1 style="color:red;width:300px" width="300">Hi!</h1>
// <p><strong style="text-decoration:none">Yes!</strong></p>
// </body>
// </html>
package premailer

View file

@ -0,0 +1,70 @@
package premailer
import (
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/vanng822/css"
"sort"
"strings"
)
type elementRules struct {
element *goquery.Selection
rules []*styleRule
cssToAttributes bool
}
func (er *elementRules) inline() {
inline, _ := er.element.Attr("style")
var inlineStyles map[string]*css.CSSStyleDeclaration
if inline != "" {
inlineStyles = css.ParseBlock(inline)
}
styles := make(map[string]string)
for _, rule := range er.rules {
for prop, s := range rule.styles {
styles[prop] = s.Value
}
}
if len(inlineStyles) > 0 {
for prop, s := range inlineStyles {
styles[prop] = s.Value
}
}
final := make([]string, 0, len(styles))
for p, v := range styles {
final = append(final, fmt.Sprintf("%s:%s", p, v))
if er.cssToAttributes {
er.style_to_basic_html_attribute(p, v)
}
}
sort.Strings(final)
style := strings.Join(final, ";")
if style != "" {
er.element.SetAttr("style", style)
}
}
func (er *elementRules) style_to_basic_html_attribute(prop, value string) {
switch prop {
case "text-align":
er.element.SetAttr("align", value)
case "vertical-align":
er.element.SetAttr("valign", value)
case "background-color":
er.element.SetAttr("bgcolor", value)
case "width":
fallthrough
case "height":
if strings.HasSuffix(value, "px") {
value = value[:len(value)-2]
}
er.element.SetAttr(prop, value)
}
}

View file

@ -0,0 +1,16 @@
package premailer
import (
"fmt"
"os"
"testing"
)
func TestMain(m *testing.M) {
fmt.Println("Test starting")
args := os.Args[:]
retCode := m.Run()
os.Args = args
fmt.Println("Test ending")
os.Exit(retCode)
}

View file

@ -0,0 +1,20 @@
package premailer
import ()
// Options for controlling behaviour
type Options struct {
// Remove class attribute from element
// Default false
RemoveClasses bool
// Copy related CSS properties into HTML attributes (e.g. background-color to bgcolor)
// Default true
CssToAttributes bool
}
// NewOptions return an Options instance with default value
func NewOptions() *Options {
options := &Options{}
options.CssToAttributes = true
return options
}

View file

@ -0,0 +1,208 @@
package premailer
import (
"fmt"
"log"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"github.com/PuerkitoBio/goquery"
"github.com/vanng822/css"
"golang.org/x/net/html"
)
// Inteface of Premailer
type Premailer interface {
// Transform process and inlining css
// It start to collect the rules in the document style tags
// Calculate specificity and sort the rules based on that
// It then collects the affected elements
// And applies the rules on those
// The leftover rules will put back into a style element
Transform() (string, error)
}
var unmergableSelector = regexp.MustCompile("(?i)\\:{1,2}(visited|active|hover|focus|link|root|in-range|invalid|valid|after|before|selection|target|first\\-(line|letter))|^\\@")
var notSupportedSelector = regexp.MustCompile("(?i)\\:(checked|disabled|enabled|lang)")
type premailer struct {
doc *goquery.Document
elIdAttr string
elements map[string]*elementRules
rules []*styleRule
leftover []*css.CSSRule
allRules [][]*css.CSSRule
elementId int
processed bool
options *Options
}
// NewPremailer return a new instance of Premailer
// It take a Document as argument and it shouldn't be nil
func NewPremailer(doc *goquery.Document, options *Options) Premailer {
pr := premailer{}
pr.doc = doc
pr.rules = make([]*styleRule, 0)
pr.allRules = make([][]*css.CSSRule, 0)
pr.leftover = make([]*css.CSSRule, 0)
pr.elements = make(map[string]*elementRules)
pr.elIdAttr = "pr-el-id"
if options == nil {
options = NewOptions()
}
pr.options = options
return &pr
}
func (pr *premailer) sortRules() {
ruleIndex := 0
for ruleSetIndex, rules := range pr.allRules {
if rules == nil {
continue
}
for _, rule := range rules {
if rule.Type != css.STYLE_RULE {
pr.leftover = append(pr.leftover, rule)
continue
}
normalStyles := make(map[string]*css.CSSStyleDeclaration)
importantStyles := make(map[string]*css.CSSStyleDeclaration)
for prop, s := range rule.Style.Styles {
if s.Important == 1 {
importantStyles[prop] = s
} else {
normalStyles[prop] = s
}
}
selectors := strings.Split(rule.Style.SelectorText, ",")
for _, selector := range selectors {
if unmergableSelector.MatchString(selector) || notSupportedSelector.MatchString(selector) {
// cause longer css
pr.leftover = append(pr.leftover, copyRule(selector, rule))
continue
}
if strings.Contains(selector, "*") {
// keep this?
pr.leftover = append(pr.leftover, copyRule(selector, rule))
continue
}
if len(normalStyles) > 0 {
pr.rules = append(pr.rules, &styleRule{makeSpecificity(0, ruleSetIndex, ruleIndex, selector), selector, normalStyles})
ruleIndex += 1
}
if len(importantStyles) > 0 {
pr.rules = append(pr.rules, &styleRule{makeSpecificity(1, ruleSetIndex, ruleIndex, selector), selector, importantStyles})
ruleIndex += 1
}
}
}
}
sort.Sort(bySpecificity(pr.rules))
}
func (pr *premailer) collectRules() {
var wg sync.WaitGroup
pr.doc.Find("style:not([data-premailer='ignore'])").Each(func(_ int, s *goquery.Selection) {
if _, exist := s.Attr("media"); exist {
return
}
wg.Add(1)
pr.allRules = append(pr.allRules, nil)
go func(ruleSetIndex int) {
defer func() {
wg.Done()
if r := recover(); r != nil {
pr.allRules[ruleSetIndex] = nil
log.Println("Got error when passing css")
log.Println(r)
}
}()
ss := css.Parse(s.Text())
pr.allRules[ruleSetIndex] = ss.GetCSSRuleList()
s.ReplaceWithHtml("")
}(len(pr.allRules) - 1)
})
wg.Wait()
}
func (pr *premailer) collectElements() {
for _, rule := range pr.rules {
pr.doc.Find(rule.selector).Each(func(_ int, s *goquery.Selection) {
if id, exist := s.Attr(pr.elIdAttr); exist {
pr.elements[id].rules = append(pr.elements[id].rules, rule)
} else {
id := strconv.Itoa(pr.elementId)
s.SetAttr(pr.elIdAttr, id)
rules := make([]*styleRule, 0)
rules = append(rules, rule)
pr.elements[id] = &elementRules{element: s, rules: rules, cssToAttributes: pr.options.CssToAttributes}
pr.elementId += 1
}
})
}
}
func (pr *premailer) applyInline() {
for _, element := range pr.elements {
element.inline()
element.element.RemoveAttr(pr.elIdAttr)
if pr.options.RemoveClasses {
element.element.RemoveAttr("class")
}
}
}
func (pr *premailer) addLeftover() {
if len(pr.leftover) > 0 {
headNode := pr.doc.Find("head")
styleNode := &html.Node{}
styleNode.Type = html.ElementNode
styleNode.Data = "style"
styleNode.Attr = []html.Attribute{html.Attribute{Key: "type", Val: "text/css"}}
cssNode := &html.Node{}
cssData := make([]string, 0, len(pr.leftover))
for _, rule := range pr.leftover {
if rule.Type == css.MEDIA_RULE {
mcssData := make([]string, 0, len(rule.Rules))
for _, mrule := range rule.Rules {
mcssData = append(mcssData, makeRuleImportant(mrule))
}
cssData = append(cssData, fmt.Sprintf("%s %s{\n%s\n}\n",
rule.Type.Text(),
rule.Style.SelectorText,
strings.Join(mcssData, "\n")))
} else {
cssData = append(cssData, makeRuleImportant(rule))
}
}
cssNode.Data = strings.Join(cssData, "")
cssNode.Type = html.TextNode
styleNode.AppendChild(cssNode)
headNode.AppendNodes(styleNode)
}
}
// Transform process and inlining css
// It start to collect the rules in the document style tags
// Calculate specificity and sort the rules based on that
// It then collects the affected elements
// And applies the rules on those
// The leftover rules will put back into a style element
func (pr *premailer) Transform() (string, error) {
if !pr.processed {
pr.collectRules()
pr.sortRules()
pr.collectElements()
pr.applyInline()
pr.addLeftover()
}
return pr.doc.Html()
}

View file

@ -0,0 +1,24 @@
package premailer
import (
"github.com/PuerkitoBio/goquery"
"os"
)
// NewPremailerFromFile take an filename
// Read the content of this file
// and create a goquery.Document
// and then create and Premailer instance.
// It will panic if any error happens
func NewPremailerFromFile(filename string, options *Options) Premailer {
fd, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fd.Close()
d, err := goquery.NewDocumentFromReader(fd)
if err != nil {
panic(err)
}
return NewPremailer(d, options)
}

View file

@ -0,0 +1,24 @@
package premailer
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestBasicHTMLFromFile(t *testing.T) {
p := NewPremailerFromFile("data/markup_test.html", nil)
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 style=\"color:red;width:50px\" width=\"50\">Hi!</h1>")
assert.Contains(t, result_html, "<h2 style=\"vertical-align:top\" valign=\"top\">There</h2>")
assert.Contains(t, result_html, "<h3 style=\"text-align:right\" align=\"right\">Hello</h3>")
assert.Contains(t, result_html, "<p><strong style=\"text-decoration:none\">Yes!</strong></p>")
assert.Contains(t, result_html, "<div style=\"background-color:green\" bgcolor=\"green\">Green color</div>")
}
func TestFromFilePanic(t *testing.T) {
assert.Panics(t, func() {
NewPremailerFromFile("data/blablabla.html", nil)
})
}

View file

@ -0,0 +1,19 @@
package premailer
import (
"github.com/PuerkitoBio/goquery"
"strings"
)
// NewPremailerFromString take in a document in string format
// and create a goquery.Document
// and then create and Premailer instance.
// It will panic if any error happens
func NewPremailerFromString(doc string, options *Options) Premailer {
read := strings.NewReader(doc)
d, err := goquery.NewDocumentFromReader(read)
if err != nil {
panic(err)
}
return NewPremailer(d, options)
}

View file

@ -0,0 +1,406 @@
package premailer
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBasicHTML(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
h1 {
width: 50px;
color:red;
}
h2 {
vertical-align: top;
}
h3 {
text-align: right;
}
strong {
text-decoration:none
}
div {
background-color: green
}
</style>
</head>
<body>
<h1>Hi!</h1>
<h2>There</h2>
<h3>Hello</h3>
<p><strong>Yes!</strong></p>
<div>Green color</div>
</body>
</html>`
p := NewPremailerFromString(html, nil)
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 style=\"color:red;width:50px\" width=\"50\">Hi!</h1>")
assert.Contains(t, result_html, "<h2 style=\"vertical-align:top\" valign=\"top\">There</h2>")
assert.Contains(t, result_html, "<h3 style=\"text-align:right\" align=\"right\">Hello</h3>")
assert.Contains(t, result_html, "<p><strong style=\"text-decoration:none\">Yes!</strong></p>")
assert.Contains(t, result_html, "<div style=\"background-color:green\" bgcolor=\"green\">Green color</div>")
}
func TestDataPremailerIgnore(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css" data-premailer="ignore">
h1, h2 {
color:red;
}
strong {
text-decoration:none
}
</style>
</head>
<body>
<h1>Hi!</h1>
<p><strong>Yes!</strong></p>
</body>
</html>`
p := NewPremailerFromString(html, nil)
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1>Hi!</h1>")
assert.Contains(t, result_html, "<p><strong>Yes!</strong></p>")
}
func TestWithInline(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
h1, h2 {
width: 50px;
color:red;
}
strong {
text-decoration:none
}
</style>
</head>
<body>
<h1 style="width: 100%;">Hi!</h1>
<p><strong>Yes!</strong></p>
</body>
</html>`
p := NewPremailerFromString(html, nil)
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 style=\"color:red;width:100%\" width=\"100%\">Hi!</h1>")
assert.Contains(t, result_html, "<p><strong style=\"text-decoration:none\">Yes!</strong></p>")
assert.NotContains(t, result_html, "<style type=\"text/css\">")
}
func TestPseudoSelectors(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
a:active {
color: red;
font-size: 12px;
}
a:first-child {
color: green;
}
</style>
</head>
<body>
<h1>Hi!</h1>
<p>
<a href="/home">Yes!</a>
<a href="/away">No!</a>
</p>
</body>
</html>`
p := NewPremailerFromString(html, nil)
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<a href=\"/home\" style=\"color:green\">Yes!</a>")
assert.Contains(t, result_html, "<style type=\"text/css\">")
}
func TestRemoveClass(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
h1, h2 {
color:red;
}
.big {
font-size: 40px;
width: 150px;
}
</style>
</head>
<body>
<h1 class="big">Hi!</h1>
<p><strong>Yes!</strong></p>
</body>
</html>`
options := &Options{}
options.RemoveClasses = true
p := NewPremailerFromString(html, options)
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 style=\"color:red;font-size:40px;width:150px\">Hi!</h1>")
assert.Contains(t, result_html, "<p><strong>Yes!</strong></p>")
}
func TestCssToAttributesFalse(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
h1, h2 {
color:red;
}
.wide {
width: 1000px;
}
</style>
</head>
<body>
<h1 class="wide">Hi!</h1>
<p><strong>Yes!</strong></p>
</body>
</html>`
options := &Options{}
options.CssToAttributes = false
p := NewPremailerFromString(html, options)
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 class=\"wide\" style=\"color:red;width:1000px\">Hi!</h1>")
assert.Contains(t, result_html, "<p><strong>Yes!</strong></p>")
}
func TestWithImportant(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
h1, h2 {
color:red;
}
p {
width: 100px !important;
color: blue
}
.wide {
width: 1000px;
}
</style>
</head>
<body>
<h1>Hi!</h1>
<p class="wide"><strong>Yes!</strong></p>
</body>
</html>`
p := NewPremailerFromString(html, NewOptions())
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 style=\"color:red\">Hi!</h1>")
assert.Contains(t, result_html, "<p class=\"wide\" style=\"color:blue;width:100px\" width=\"100\"><strong>Yes!</strong></p>")
}
func TestWithMediaRule(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
h1, h2 {
color:red;
}
p {
width: 100px !important;
color: blue
}
.wide {
width: 1000px;
}
@media all and (min-width: 62em) {
h1 {
font-size: 55px;
line-height: 60px;
padding-top: 0;
padding-bottom: 5px
}
}
</style>
</head>
<body>
<h1>Hi!</h1>
<p class="wide"><strong>Yes!</strong></p>
</body>
</html>`
p := NewPremailerFromString(html, NewOptions())
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 style=\"color:red\">Hi!</h1>")
assert.Contains(t, result_html, "<p class=\"wide\" style=\"color:blue;width:100px\" width=\"100\"><strong>Yes!</strong></p>")
assert.Contains(t, result_html, "@media all and (min-width: 62em){")
assert.Contains(t, result_html, "font-size: 55px !important;")
assert.Contains(t, result_html, "line-height: 60px !important;")
assert.Contains(t, result_html, "padding-bottom: 5px !important;")
assert.Contains(t, result_html, "padding-top: 0 !important")
}
func TestWithMediaAttribute(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
h1, h2 {
color:red;
}
p {
width: 100px !important;
color: blue
}
.wide {
width: 1000px;
}
</style>
<style type="text/css" media="all and (min-width: 62em)">
h1 {
font-size: 55px;
line-height: 60px;
padding-top: 0;
padding-bottom: 5px
}
</style>
<style>
</style>
</head>
<body>
<h1>Hi!</h1>
<p class="wide"><strong>Yes!</strong></p>
</body>
</html>`
p := NewPremailerFromString(html, NewOptions())
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 style=\"color:red\">Hi!</h1>")
assert.Contains(t, result_html, "<p class=\"wide\" style=\"color:blue;width:100px\" width=\"100\"><strong>Yes!</strong></p>")
assert.Contains(t, result_html, "<style type=\"text/css\" media=\"all and (min-width: 62em)\">")
assert.Contains(t, result_html, "font-size: 55px;")
assert.Contains(t, result_html, "line-height: 60px;")
assert.Contains(t, result_html, "padding-top: 0;")
assert.Contains(t, result_html, "padding-bottom: 5px")
}
func TestIndexOutOfRange(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
h1, h2 {
color:red;
}
p {
width: 100px !important;
color: blue
}
.wide {
width: 1000px;
}
</style>
<style type="text/css" media="all and (min-width: 62em)">
h1 {
font-size: 55px;
line-height: 60px;
padding-top: 0;
padding-bottom: 5px
}
</style>
<style>
.some {
color: red;
}
</style>
</head>
<body>
<h1>Hi!</h1>
<p class="wide"><strong>Yes!</strong></p>
</body>
</html>`
p := NewPremailerFromString(html, NewOptions())
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, "<h1 style=\"color:red\">Hi!</h1>")
assert.Contains(t, result_html, "<p class=\"wide\" style=\"color:blue;width:100px\" width=\"100\"><strong>Yes!</strong></p>")
assert.Contains(t, result_html, "<style type=\"text/css\" media=\"all and (min-width: 62em)\">")
assert.Contains(t, result_html, "font-size: 55px;")
assert.Contains(t, result_html, "line-height: 60px;")
assert.Contains(t, result_html, "padding-top: 0;")
assert.Contains(t, result_html, "padding-bottom: 5px")
}
func TestSpecificity(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css">
table.bar-chart td.bar-area {
padding: 10px;
}
table { width: 91%; }
table { width: 92%; }
table { width: 93%; }
table { width: 94%; }
table { width: 95%; }
table { width: 96%; }
table { width: 97%; }
table.bar-chart td {
padding: 5px;
}
</style>
</head>
<body>
<table class="bar-chart">
<tr><td>1</td></tr>
<tr><td class="bar-area">2</td></tr>
</table>
</body>
</html>`
p := NewPremailerFromString(html, NewOptions())
result_html, err := p.Transform()
assert.Nil(t, err)
assert.Contains(t, result_html, `<tr><td style="padding:5px">1</td></tr>`)
assert.Contains(t, result_html, `<tr><td class="bar-area" style="padding:10px">2</td></tr>`)
}

View file

@ -0,0 +1,112 @@
package premailer
import (
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"strings"
"testing"
)
func TestSupportedSelectors(t *testing.T) {
selectors := []string{
".footer__content_wrapper--last",
"table[class=\"body\"] .footer__content td",
"table[class=\"body\"] td.footer__link_wrapper--first",
".header + .content",
"#firstname",
"p ~ ul",
"div > p",
"div > p",
"div p",
"div, p",
"[target]",
"[target=_blank]",
"[title~=flower]",
"[lang|=en]",
"a[href^=\"https\"]",
"a[href$=\".pdf\"]",
"a[href*=\"css\"]",
"p:empty",
"p:first-child",
"p:first-of-type",
"p:last-child",
"p:last-of-type",
":not(p)",
"p:nth-child(2)",
"p:nth-last-child(2)",
"p:nth-of-type(2)",
"p:only-child",
"p:nth-last-of-type(2)",
"div:not(:nth-child(1))",
"div:not(:not(:first-child))",
}
html := `<html>
<head>
<title>Title</title>
<style type="text/css" data-premailer="ignore">
h1, h2 {
color:red;
}
strong {
text-decoration:none
}
</style>
</head>
<body>
<h1>Hi!</h1>
<p><strong>Yes!</strong></p>
</body>
</html>`
read := strings.NewReader(html)
doc, _ := goquery.NewDocumentFromReader(read)
pr := premailer{}
pr.doc = doc
for _, selector := range selectors {
assert.NotPanics(t, func() {
pr.doc.Find(selector)
})
}
}
func TestNotSupportedSelectors(t *testing.T) {
html := `<html>
<head>
<title>Title</title>
<style type="text/css" data-premailer="ignore">
h1, h2 {
color:red;
}
strong {
text-decoration:none
}
</style>
</head>
<body>
<h1>Hi!</h1>
<p><strong>Yes!</strong></p>
</body>
</html>`
read := strings.NewReader(html)
doc, _ := goquery.NewDocumentFromReader(read)
pr := premailer{}
pr.doc = doc
notSupportedSelectors := []string{
"input:checked",
"input:disabled",
"input:enabled",
"input:optional",
"input:read-only",
"p:lang(it)",
}
for _, selector := range notSupportedSelectors {
assert.Equal(t, 0, pr.doc.Find(selector).Length())
}
}

View file

@ -0,0 +1,68 @@
package premailer
import (
"regexp"
"strings"
)
// https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity
// https://developer.mozilla.org/en-US/docs/Web/CSS/Reference#Selectors
type specificity struct {
important int
idCount int
classCount int
typeCount int
attrCount int
ruleSetIndex int
ruleIndex int
}
func (s *specificity) importantOrders() []int {
return []int{s.important, s.idCount,
s.classCount, s.attrCount,
s.typeCount, s.ruleSetIndex,
s.ruleIndex}
}
var _type_selector_regex = regexp.MustCompile("(^|\\s)\\w")
func makeSpecificity(important, ruleSetIndex, ruleIndex int, selector string) *specificity {
spec := specificity{}
// determine values for priority
if important > 0 {
spec.important = 1
} else {
spec.important = 0
}
spec.idCount = strings.Count(selector, "#")
spec.classCount = strings.Count(selector, ".")
spec.attrCount = strings.Count(selector, "[")
spec.typeCount = len(_type_selector_regex.FindAllString(selector, -1))
spec.ruleSetIndex = ruleSetIndex
spec.ruleIndex = ruleIndex
return &spec
}
type bySpecificity []*styleRule
func (bs bySpecificity) Len() int {
return len(bs)
}
func (bs bySpecificity) Swap(i, j int) {
bs[i], bs[j] = bs[j], bs[i]
}
func (bs bySpecificity) Less(i, j int) bool {
iorders := bs[i].specificity.importantOrders()
jorders := bs[j].specificity.importantOrders()
for n, v := range iorders {
if v < jorders[n] {
return true
}
if v > jorders[n] {
return false
}
}
return false
}

View file

@ -0,0 +1,118 @@
package premailer
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSpecificitySelectorType(t *testing.T) {
spec := makeSpecificity(1, 2, 100, "table")
expected := []int{1, 0, 0, 0, 1, 2, 100}
assert.Equal(t, expected, spec.importantOrders())
}
func TestSpecificitySelectorClass(t *testing.T) {
// class
spec := makeSpecificity(1, 2, 102, "table.red")
expected := []int{1, 0, 1, 0, 1, 2, 102}
assert.Equal(t, expected, spec.importantOrders())
}
func TestSpecificitySelectorAttr(t *testing.T) {
// Attribute
spec := makeSpecificity(1, 3, 103, "span[lang~=\"en-us\"]")
expected := []int{1, 0, 0, 1, 1, 3, 103}
assert.Equal(t, expected, spec.importantOrders())
}
func TestSpecificitySelectorId(t *testing.T) {
// id
spec := makeSpecificity(0, 3, 104, "#example")
expected := []int{0, 1, 0, 0, 0, 3, 104}
assert.Equal(t, expected, spec.importantOrders())
}
func TestSpecificitySort(t *testing.T) {
undertest := make([]*styleRule, 4)
for i := 0; i < 4; i++ {
undertest[i] = &styleRule{}
}
specificity0 := makeSpecificity(1, 2, 100, "table")
undertest[0].specificity = specificity0
specificity1 := makeSpecificity(1, 2, 102, "table.red")
undertest[1].specificity = specificity1
specificity2 := makeSpecificity(1, 3, 103, "span[lang~=\"en-us\"]")
undertest[2].specificity = specificity2
specificity3 := makeSpecificity(0, 3, 104, "#example")
undertest[3].specificity = specificity3
// expected order
/*
expected3 := []int{0, 1, 0, 0, 0, 3, 104}
expected0 := []int{1, 0, 0, 0, 1, 2, 100}
expected2 := []int{1, 0, 0, 1, 1, 3, 103}
expected1 := []int{1, 0, 1, 0, 1, 2, 102}
*/
sort.Sort(bySpecificity(undertest))
assert.Equal(t, specificity3, undertest[0].specificity)
assert.Equal(t, specificity0, undertest[1].specificity)
assert.Equal(t, specificity2, undertest[2].specificity)
assert.Equal(t, specificity1, undertest[3].specificity)
}
func TestSpecificitySortRuleSetIndex(t *testing.T) {
undertest := make([]*styleRule, 2)
for i := 0; i < 2; i++ {
undertest[i] = &styleRule{}
}
specificity0 := makeSpecificity(1, 2, 102, "table")
undertest[0].specificity = specificity0
specificity1 := makeSpecificity(1, 1, 102, "table")
undertest[1].specificity = specificity1
sort.Sort(bySpecificity(undertest))
assert.Equal(t, specificity1, undertest[0].specificity)
assert.Equal(t, specificity0, undertest[1].specificity)
}
func TestSpecificitySortRuleIndex(t *testing.T) {
undertest := make([]*styleRule, 2)
for i := 0; i < 2; i++ {
undertest[i] = &styleRule{}
}
specificity0 := makeSpecificity(1, 1, 102, "table")
undertest[0].specificity = specificity0
specificity1 := makeSpecificity(1, 1, 100, "table")
undertest[1].specificity = specificity1
sort.Sort(bySpecificity(undertest))
assert.Equal(t, specificity1, undertest[0].specificity)
assert.Equal(t, specificity0, undertest[1].specificity)
}
func TestSpecificitySortLongArray(t *testing.T) {
// It has to be longer than 6 due to internal implementation of sort.Sort(),
rules := []*styleRule{
&styleRule{specificity: makeSpecificity(0, 0, 1, "table.padded")},
&styleRule{specificity: makeSpecificity(0, 0, 2, "table.padded")},
&styleRule{specificity: makeSpecificity(0, 0, 3, "table.padded")},
&styleRule{specificity: makeSpecificity(0, 0, 4, "table.padded")},
&styleRule{specificity: makeSpecificity(0, 0, 5, "table.padded")},
&styleRule{specificity: makeSpecificity(0, 0, 6, "table.padded")},
&styleRule{specificity: makeSpecificity(0, 0, 11, "table")},
}
sort.Sort(bySpecificity(rules))
ruleIndices := make([]int, len(rules))
for i := range rules {
ruleIndices[i] = rules[i].specificity.ruleIndex
}
expectedRuleIndices := []int{11, 1, 2, 3, 4, 5, 6}
assert.Equal(t, expectedRuleIndices, ruleIndices)
}

View file

@ -0,0 +1,11 @@
package premailer
import (
"github.com/vanng822/css"
)
type styleRule struct {
specificity *specificity
selector string
styles map[string]*css.CSSStyleDeclaration
}

View file

@ -0,0 +1,24 @@
package premailer
import (
"github.com/vanng822/css"
)
func copyRule(selector string, rule *css.CSSRule) *css.CSSRule {
// copy rule for each selector
styles := make(map[string]*css.CSSStyleDeclaration)
for prop, s := range rule.Style.Styles {
styles[prop] = css.NewCSSStyleDeclaration(s.Property, s.Value, s.Important)
}
copiedStyle := css.CSSStyleRule{SelectorText: selector, Styles: styles}
copiedRule := &css.CSSRule{Type: rule.Type, Style: copiedStyle}
return copiedRule
}
func makeRuleImportant(rule *css.CSSRule) string {
// this for using Text() which has nice sorted props
for _, s := range rule.Style.Styles {
s.Important = 1
}
return rule.Style.Text()
}