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

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()
}