1package inliner 2 3import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "github.com/PuerkitoBio/goquery" 9 "github.com/aymerick/douceur/css" 10 "github.com/aymerick/douceur/parser" 11 "golang.org/x/net/html" 12) 13 14const ( 15 eltMarkerAttr = "douceur-mark" 16) 17 18var unsupportedSelectors = []string{ 19 ":active", ":after", ":before", ":checked", ":disabled", ":enabled", 20 ":first-line", ":first-letter", ":focus", ":hover", ":invalid", ":in-range", 21 ":lang", ":link", ":root", ":selection", ":target", ":valid", ":visited"} 22 23// Inliner presents a CSS Inliner 24type Inliner struct { 25 // Raw HTML 26 html string 27 28 // Parsed HTML document 29 doc *goquery.Document 30 31 // Parsed stylesheets 32 stylesheets []*css.Stylesheet 33 34 // Collected inlinable style rules 35 rules []*StyleRule 36 37 // HTML elements matching collected inlinable style rules 38 elements map[string]*Element 39 40 // CSS rules that are not inlinable but that must be inserted in output document 41 rawRules []fmt.Stringer 42 43 // current element marker value 44 eltMarker int 45} 46 47// NewInliner instanciates a new Inliner 48func NewInliner(html string) *Inliner { 49 return &Inliner{ 50 html: html, 51 elements: make(map[string]*Element), 52 } 53} 54 55// Inline inlines css into html document 56func Inline(html string) (string, error) { 57 result, err := NewInliner(html).Inline() 58 if err != nil { 59 return "", err 60 } 61 62 return result, nil 63} 64 65// Inline inlines CSS and returns HTML 66func (inliner *Inliner) Inline() (string, error) { 67 // parse HTML document 68 if err := inliner.parseHTML(); err != nil { 69 return "", err 70 } 71 72 // parse stylesheets 73 if err := inliner.parseStylesheets(); err != nil { 74 return "", err 75 } 76 77 // collect elements and style rules 78 inliner.collectElementsAndRules() 79 80 // inline css 81 if err := inliner.inlineStyleRules(); err != nil { 82 return "", err 83 } 84 85 // insert raw stylesheet 86 inliner.insertRawStylesheet() 87 88 // generate HTML document 89 return inliner.genHTML() 90} 91 92// Parses raw html 93func (inliner *Inliner) parseHTML() error { 94 doc, err := goquery.NewDocumentFromReader(strings.NewReader(inliner.html)) 95 if err != nil { 96 return err 97 } 98 99 inliner.doc = doc 100 101 return nil 102} 103 104// Parses and removes stylesheets from HTML document 105func (inliner *Inliner) parseStylesheets() error { 106 var result error 107 108 inliner.doc.Find("style").EachWithBreak(func(i int, s *goquery.Selection) bool { 109 stylesheet, err := parser.Parse(s.Text()) 110 if err != nil { 111 result = err 112 return false 113 } 114 115 inliner.stylesheets = append(inliner.stylesheets, stylesheet) 116 117 // removes parsed stylesheet 118 s.Remove() 119 120 return true 121 }) 122 123 return result 124} 125 126// Collects HTML elements matching parsed stylesheets, and thus collect used style rules 127func (inliner *Inliner) collectElementsAndRules() { 128 for _, stylesheet := range inliner.stylesheets { 129 for _, rule := range stylesheet.Rules { 130 if rule.Kind == css.QualifiedRule { 131 // Let's go! 132 inliner.handleQualifiedRule(rule) 133 } else { 134 // Keep it 'as is' 135 inliner.rawRules = append(inliner.rawRules, rule) 136 } 137 } 138 } 139} 140 141// Handles parsed qualified rule 142func (inliner *Inliner) handleQualifiedRule(rule *css.Rule) { 143 for _, selector := range rule.Selectors { 144 if Inlinable(selector) { 145 inliner.doc.Find(selector).Each(func(i int, s *goquery.Selection) { 146 // get marker 147 eltMarker, exists := s.Attr(eltMarkerAttr) 148 if !exists { 149 // mark element 150 eltMarker = strconv.Itoa(inliner.eltMarker) 151 s.SetAttr(eltMarkerAttr, eltMarker) 152 inliner.eltMarker++ 153 154 // add new element 155 inliner.elements[eltMarker] = NewElement(s) 156 } 157 158 // add style rule for element 159 inliner.elements[eltMarker].addStyleRule(NewStyleRule(selector, rule.Declarations)) 160 }) 161 } else { 162 // Keep it 'as is' 163 inliner.rawRules = append(inliner.rawRules, NewStyleRule(selector, rule.Declarations)) 164 } 165 } 166} 167 168// Inline style rules in HTML document 169func (inliner *Inliner) inlineStyleRules() error { 170 for _, element := range inliner.elements { 171 // remove marker 172 element.elt.RemoveAttr(eltMarkerAttr) 173 174 // inline element 175 err := element.inline() 176 if err != nil { 177 return err 178 } 179 } 180 181 return nil 182} 183 184// Computes raw CSS rules 185func (inliner *Inliner) computeRawCSS() string { 186 result := "" 187 188 for _, rawRule := range inliner.rawRules { 189 result += rawRule.String() 190 result += "\n" 191 } 192 193 return result 194} 195 196// Insert raw CSS rules into HTML document 197func (inliner *Inliner) insertRawStylesheet() { 198 rawCSS := inliner.computeRawCSS() 199 if rawCSS != "" { 200 // create <style> element 201 cssNode := &html.Node{ 202 Type: html.TextNode, 203 Data: "\n" + rawCSS, 204 } 205 206 styleNode := &html.Node{ 207 Type: html.ElementNode, 208 Data: "style", 209 Attr: []html.Attribute{html.Attribute{Key: "type", Val: "text/css"}}, 210 } 211 212 styleNode.AppendChild(cssNode) 213 214 // append to <head> element 215 headNode := inliner.doc.Find("head") 216 if headNode == nil { 217 // @todo Create head node ! 218 panic("NOT IMPLEMENTED: create missing <head> node") 219 } 220 221 headNode.AppendNodes(styleNode) 222 } 223} 224 225// Generates HTML 226func (inliner *Inliner) genHTML() (string, error) { 227 return inliner.doc.Html() 228} 229 230// Inlinable returns true if given selector is inlinable 231func Inlinable(selector string) bool { 232 if strings.Contains(selector, "::") { 233 return false 234 } 235 236 for _, badSel := range unsupportedSelectors { 237 if strings.Contains(selector, badSel) { 238 return false 239 } 240 } 241 242 return true 243} 244