| package inliner |
| |
| import ( |
| "fmt" |
| "strconv" |
| "strings" |
| |
| "github.com/PuerkitoBio/goquery" |
| "github.com/chris-ramon/douceur/css" |
| "github.com/chris-ramon/douceur/parser" |
| "golang.org/x/net/html" |
| ) |
| |
| const ( |
| eltMarkerAttr = "douceur-mark" |
| ) |
| |
| var unsupportedSelectors = []string{ |
| ":active", ":after", ":before", ":checked", ":disabled", ":enabled", |
| ":first-line", ":first-letter", ":focus", ":hover", ":invalid", ":in-range", |
| ":lang", ":link", ":root", ":selection", ":target", ":valid", ":visited"} |
| |
| // Inliner presents a CSS Inliner |
| type Inliner struct { |
| // Raw HTML |
| html string |
| |
| // Parsed HTML document |
| doc *goquery.Document |
| |
| // Parsed stylesheets |
| stylesheets []*css.Stylesheet |
| |
| // Collected inlinable style rules |
| rules []*StyleRule |
| |
| // HTML elements matching collected inlinable style rules |
| elements map[string]*Element |
| |
| // CSS rules that are not inlinable but that must be inserted in output document |
| rawRules []fmt.Stringer |
| |
| // current element marker value |
| eltMarker int |
| } |
| |
| // NewInliner instanciates a new Inliner |
| func NewInliner(html string) *Inliner { |
| return &Inliner{ |
| html: html, |
| elements: make(map[string]*Element), |
| } |
| } |
| |
| // Inline inlines css into html document |
| func Inline(html string) (string, error) { |
| result, err := NewInliner(html).Inline() |
| if err != nil { |
| return "", err |
| } |
| |
| return result, nil |
| } |
| |
| // Inline inlines CSS and returns HTML |
| func (inliner *Inliner) Inline() (string, error) { |
| // parse HTML document |
| if err := inliner.parseHTML(); err != nil { |
| return "", err |
| } |
| |
| // parse stylesheets |
| if err := inliner.parseStylesheets(); err != nil { |
| return "", err |
| } |
| |
| // collect elements and style rules |
| inliner.collectElementsAndRules() |
| |
| // inline css |
| if err := inliner.inlineStyleRules(); err != nil { |
| return "", err |
| } |
| |
| // insert raw stylesheet |
| inliner.insertRawStylesheet() |
| |
| // generate HTML document |
| return inliner.genHTML() |
| } |
| |
| // Parses raw html |
| func (inliner *Inliner) parseHTML() error { |
| doc, err := goquery.NewDocumentFromReader(strings.NewReader(inliner.html)) |
| if err != nil { |
| return err |
| } |
| |
| inliner.doc = doc |
| |
| return nil |
| } |
| |
| // Parses and removes stylesheets from HTML document |
| func (inliner *Inliner) parseStylesheets() error { |
| var result error |
| |
| inliner.doc.Find("style").EachWithBreak(func(i int, s *goquery.Selection) bool { |
| stylesheet, err := parser.Parse(s.Text()) |
| if err != nil { |
| result = err |
| return false |
| } |
| |
| inliner.stylesheets = append(inliner.stylesheets, stylesheet) |
| |
| // removes parsed stylesheet |
| s.Remove() |
| |
| return true |
| }) |
| |
| return result |
| } |
| |
| // Collects HTML elements matching parsed stylesheets, and thus collect used style rules |
| func (inliner *Inliner) collectElementsAndRules() { |
| for _, stylesheet := range inliner.stylesheets { |
| for _, rule := range stylesheet.Rules { |
| if rule.Kind == css.QualifiedRule { |
| // Let's go! |
| inliner.handleQualifiedRule(rule) |
| } else { |
| // Keep it 'as is' |
| inliner.rawRules = append(inliner.rawRules, rule) |
| } |
| } |
| } |
| } |
| |
| // Handles parsed qualified rule |
| func (inliner *Inliner) handleQualifiedRule(rule *css.Rule) { |
| for _, selector := range rule.Selectors { |
| if Inlinable(selector.Value) { |
| inliner.doc.Find(selector.Value).Each(func(i int, s *goquery.Selection) { |
| // get marker |
| eltMarker, exists := s.Attr(eltMarkerAttr) |
| if !exists { |
| // mark element |
| eltMarker = strconv.Itoa(inliner.eltMarker) |
| s.SetAttr(eltMarkerAttr, eltMarker) |
| inliner.eltMarker++ |
| |
| // add new element |
| inliner.elements[eltMarker] = NewElement(s) |
| } |
| |
| // add style rule for element |
| inliner.elements[eltMarker].addStyleRule(NewStyleRule(selector.Value, rule.Declarations)) |
| }) |
| } else { |
| // Keep it 'as is' |
| inliner.rawRules = append(inliner.rawRules, NewStyleRule(selector.Value, rule.Declarations)) |
| } |
| } |
| } |
| |
| // Inline style rules in HTML document |
| func (inliner *Inliner) inlineStyleRules() error { |
| for _, element := range inliner.elements { |
| // remove marker |
| element.elt.RemoveAttr(eltMarkerAttr) |
| |
| // inline element |
| err := element.inline() |
| if err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| // Computes raw CSS rules |
| func (inliner *Inliner) computeRawCSS() string { |
| result := "" |
| |
| for _, rawRule := range inliner.rawRules { |
| result += rawRule.String() |
| result += "\n" |
| } |
| |
| return result |
| } |
| |
| // Insert raw CSS rules into HTML document |
| func (inliner *Inliner) insertRawStylesheet() { |
| rawCSS := inliner.computeRawCSS() |
| if rawCSS != "" { |
| // create <style> element |
| cssNode := &html.Node{ |
| Type: html.TextNode, |
| Data: "\n" + rawCSS, |
| } |
| |
| styleNode := &html.Node{ |
| Type: html.ElementNode, |
| Data: "style", |
| Attr: []html.Attribute{{Key: "type", Val: "text/css"}}, |
| } |
| |
| styleNode.AppendChild(cssNode) |
| |
| // append to <head> element |
| headNode := inliner.doc.Find("head") |
| if headNode == nil { |
| // @todo Create head node ! |
| panic("NOT IMPLEMENTED: create missing <head> node") |
| } |
| |
| headNode.AppendNodes(styleNode) |
| } |
| } |
| |
| // Generates HTML |
| func (inliner *Inliner) genHTML() (string, error) { |
| return inliner.doc.Html() |
| } |
| |
| // Inlinable returns true if given selector is inlinable |
| func Inlinable(selector string) bool { |
| if strings.Contains(selector, "::") { |
| return false |
| } |
| |
| for _, badSel := range unsupportedSelectors { |
| if strings.Contains(selector, badSel) { |
| return false |
| } |
| } |
| |
| return true |
| } |