f128c947ab
* 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
244 lines
5.2 KiB
Go
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
|
|
}
|