woodpecker-email/vendor/github.com/aymerick/douceur/inliner/inliner.go
Michael de Wit f128c947ab Refactor plugin to be compatible with Drone 0.5 (#13)
* Refactor plugin to be compatible with Drone 0.5

* Add vendor files

* Re-add logo.svg, make loading environment from .env file optional, and use drone-go/template

* Fix README

* Fix issue with date formatting, update the DOCS, and improve types

* Add working directory and volume mount to README example
2017-01-16 13:20:59 +01:00

244 lines
5.2 KiB
Go

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