func (c *cssMinifier) minifySelectors(property []byte, values []css.Token) error { inAttr := false isClass := false for _, val := range c.p.Values() { if !inAttr { if val.TokenType == css.IdentToken { if !isClass { parse.ToLower(val.Data) } isClass = false } else if val.TokenType == css.DelimToken && val.Data[0] == '.' { isClass = true } else if val.TokenType == css.LeftBracketToken { inAttr = true } } else { if val.TokenType == css.StringToken && len(val.Data) > 2 { s := val.Data[1 : len(val.Data)-1] if css.IsIdent([]byte(s)) { if _, err := c.w.Write(s); err != nil { return err } continue } } else if val.TokenType == css.RightBracketToken { inAttr = false } } if _, err := c.w.Write(val.Data); err != nil { return err } } return nil }
func (p *Parser) parseDeclaration() GrammarType { p.initBuf() parse.ToLower(p.data) if tt, _ := p.popToken(false); tt != ColonToken { p.err = ErrBadDeclaration return ErrorGrammar } skipWS := true for { tt, data := p.popToken(false) if (tt == SemicolonToken || tt == RightBraceToken) && p.level == 0 || tt == ErrorToken { p.prevEnd = (tt == RightBraceToken) return DeclarationGrammar } else if tt == LeftParenthesisToken || tt == LeftBraceToken || tt == LeftBracketToken || tt == FunctionToken { p.level++ } else if tt == RightParenthesisToken || tt == RightBraceToken || tt == RightBracketToken { p.level-- } if len(data) == 1 && (data[0] == ',' || data[0] == '/' || data[0] == ':' || data[0] == '!' || data[0] == '=') { skipWS = true } else if p.prevWS && !skipWS { p.pushBuf(WhitespaceToken, wsBytes) } else { skipWS = false } p.pushBuf(tt, data) } }
func (l *Lexer) shiftEndTag() []byte { for { c := l.r.Peek(0) if c == '>' { l.text = l.r.Lexeme()[2:] l.r.Move(1) break } else if c == 0 { l.text = l.r.Lexeme()[2:] break } l.r.Move(1) } end := len(l.text) for end > 0 { if c := l.text[end-1]; c == ' ' || c == '\t' || c == '\n' || c == '\r' { end-- continue } break } l.text = l.text[:end] return parse.ToLower(l.r.Shift()) }
func (l *Lexer) shiftAttribute() []byte { nameStart := l.r.Pos() var c byte for { // attribute name state if c = l.r.Peek(0); c == ' ' || c == '=' || c == '>' || c == '/' && l.r.Peek(1) == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 { break } l.r.Move(1) } nameEnd := l.r.Pos() for { // after attribute name state if c = l.r.Peek(0); c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' { l.r.Move(1) continue } break } if c == '=' { l.r.Move(1) for { // before attribute value state if c = l.r.Peek(0); c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' { l.r.Move(1) continue } break } attrPos := l.r.Pos() delim := c if delim == '"' || delim == '\'' { // attribute value single- and double-quoted state l.r.Move(1) for { c := l.r.Peek(0) if c == delim { l.r.Move(1) break } else if c == 0 { break } l.r.Move(1) } } else { // attribute value unquoted state for { if c := l.r.Peek(0); c == ' ' || c == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 { break } l.r.Move(1) } } l.attrVal = l.r.Lexeme()[attrPos:] } else { l.r.Rewind(nameEnd) l.attrVal = nil } l.text = parse.ToLower(l.r.Lexeme()[nameStart:nameEnd]) return l.r.Shift() }
// ContentType minifies a given mediatype by removing all whitespace. func ContentType(b []byte) []byte { j := 0 start := 0 inString := false for i, c := range b { if !inString && parse.IsWhitespace(c) { if start != 0 { j += copy(b[j:], b[start:i]) } else { j += i } start = i + 1 } else if c == '"' { inString = !inString } } if start != 0 { j += copy(b[j:], b[start:]) return parse.ToLower(b[:j]) } return parse.ToLower(b) }
func (l *Lexer) shiftStartTag() []byte { for { if c := l.r.Peek(0); c == ' ' || c == '>' || c == '/' && l.r.Peek(1) == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 { break } l.r.Move(1) } l.text = parse.ToLower(l.r.Lexeme()[1:]) if h := ToHash(l.text); h == Textarea || h == Title || h == Style || h == Xmp || h == Iframe || h == Script || h == Plaintext || h == Svg || h == Math { l.rawTag = h } return l.r.Shift() }
func (p *Parser) parseAtRule() GrammarType { p.initBuf() parse.ToLower(p.data) atRuleName := p.data if len(atRuleName) > 0 && atRuleName[1] == '-' { if i := bytes.IndexByte(atRuleName[2:], '-'); i != -1 { atRuleName = atRuleName[i+2:] // skip vendor specific prefix } } atRule := ToHash(atRuleName[1:]) first := true skipWS := false for { tt, data := p.popToken(false) if tt == LeftBraceToken && p.level == 0 { if atRule == Font_Face || atRule == Page { p.state = append(p.state, p.parseAtRuleDeclarationList) } else if atRule == Document || atRule == Keyframes || atRule == Media || atRule == Supports { p.state = append(p.state, p.parseAtRuleRuleList) } else { p.state = append(p.state, p.parseAtRuleUnknown) } return BeginAtRuleGrammar } else if (tt == SemicolonToken || tt == RightBraceToken) && p.level == 0 || tt == ErrorToken { p.prevEnd = (tt == RightBraceToken) return AtRuleGrammar } else if tt == LeftParenthesisToken || tt == LeftBraceToken || tt == LeftBracketToken || tt == FunctionToken { p.level++ } else if tt == RightParenthesisToken || tt == RightBraceToken || tt == RightBracketToken { p.level-- } if first { if tt == LeftParenthesisToken || tt == LeftBracketToken { p.prevWS = false } first = false } if len(data) == 1 && (data[0] == ',' || data[0] == ':') { skipWS = true } else if p.prevWS && !skipWS && tt != RightParenthesisToken { p.pushBuf(WhitespaceToken, wsBytes) } else { skipWS = false } if tt == LeftParenthesisToken { skipWS = true } p.pushBuf(tt, data) } }
func (o *Minifier) shortenDimension(b []byte) ([]byte, int) { if n, m := parse.Dimension(b); n > 0 { unit := b[n : n+m] b = minify.Number(b[:n], o.Decimals) if len(b) != 1 || b[0] != '0' { if m == 2 && unit[0] == 'p' && unit[1] == 'x' { unit = nil } else if m > 1 { // only percentage is length 1 parse.ToLower(unit) } b = append(b, unit...) } return b, n + m } return b, 0 }
func (c *cssMinifier) shortenToken(prop css.Hash, tt css.TokenType, data []byte) (css.TokenType, []byte) { if tt == css.NumberToken || tt == css.PercentageToken || tt == css.DimensionToken { if tt == css.NumberToken && (prop == css.Z_Index || prop == css.Counter_Increment || prop == css.Counter_Reset || prop == css.Orphans || prop == css.Widows) { return tt, data // integers } n := len(data) if tt == css.PercentageToken { n-- } else if tt == css.DimensionToken { n = parse.Number(data) } dim := data[n:] data = minify.Number(data[:n], c.o.Decimals) if len(data) != 1 || data[0] != '0' { if tt == css.PercentageToken { data = append(data, '%') } else if tt == css.DimensionToken { parse.ToLower(dim) data = append(data, dim...) } } } else if tt == css.IdentToken { parse.ToLower(data) if hex, ok := ShortenColorName[css.ToHash(data)]; ok { tt = css.HashToken data = hex } } else if tt == css.HashToken { parse.ToLower(data) if ident, ok := ShortenColorHex[string(data)]; ok { tt = css.IdentToken data = ident } else if len(data) == 7 && data[1] == data[2] && data[3] == data[4] && data[5] == data[6] { tt = css.HashToken data[2] = data[3] data[3] = data[5] data = data[:4] } } else if tt == css.StringToken { // remove any \\\r\n \\\r \\\n for i := 1; i < len(data)-2; i++ { if data[i] == '\\' && (data[i+1] == '\n' || data[i+1] == '\r') { // encountered first replacee, now start to move bytes to the front j := i + 2 if data[i+1] == '\r' && len(data) > i+2 && data[i+2] == '\n' { j++ } for ; j < len(data); j++ { if data[j] == '\\' && len(data) > j+1 && (data[j+1] == '\n' || data[j+1] == '\r') { if data[j+1] == '\r' && len(data) > j+2 && data[j+2] == '\n' { j++ } j++ } else { data[i] = data[j] i++ } } data = data[:i] break } } } else if tt == css.URLToken { parse.ToLower(data[:3]) if len(data) > 10 { uri := data[4 : len(data)-1] delim := byte('"') if uri[0] == '\'' || uri[0] == '"' { delim = uri[0] uri = uri[1 : len(uri)-1] } uri = minify.DataURI(c.m, uri) if css.IsURLUnquoted(uri) { data = append(append([]byte("url("), uri...), ')') } else { data = append(append(append([]byte("url("), delim), uri...), delim, ')') } } } return tt, data }
func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) { n := 1 simple := true for i, value := range values[1:] { if value.TokenType == css.RightParenthesisToken { n++ break } if i%2 == 0 && (value.TokenType != css.NumberToken && value.TokenType != css.PercentageToken) || (i%2 == 1 && value.TokenType != css.CommaToken) { simple = false } n++ } values = values[:n] if simple && (n-1)%2 == 0 { fun := css.ToHash(values[0].Data[:len(values[0].Data)-1]) nArgs := (n - 1) / 2 if (fun == css.Rgba || fun == css.Hsla) && nArgs == 4 { d, _ := strconv.ParseFloat(string(values[7].Data), 32) // can never fail because if simple == true than this is a NumberToken or PercentageToken if d-1.0 > -minify.Epsilon { if fun == css.Rgba { values[0].Data = []byte("rgb(") fun = css.Rgb } else { values[0].Data = []byte("hsl(") fun = css.Hsl } values = values[:len(values)-2] values[len(values)-1].Data = []byte(")") nArgs = 3 } else if d < minify.Epsilon { values[0].Data = []byte("transparent") values = values[:1] fun = 0 nArgs = 0 } } if fun == css.Rgb && nArgs == 3 { var err [3]error rgb := [3]byte{} for j := 0; j < 3; j++ { val := values[j*2+1] if val.TokenType == css.NumberToken { var d int64 d, err[j] = strconv.ParseInt(string(val.Data), 10, 32) if d < 0 { d = 0 } else if d > 255 { d = 255 } rgb[j] = byte(d) } else if val.TokenType == css.PercentageToken { var d float64 d, err[j] = strconv.ParseFloat(string(val.Data[:len(val.Data)-1]), 32) if d < 0.0 { d = 0.0 } else if d > 100.0 { d = 100.0 } rgb[j] = byte((d / 100.0 * 255.0) + 0.5) } } if err[0] == nil && err[1] == nil && err[2] == nil { val := make([]byte, 7) val[0] = '#' hex.Encode(val[1:], rgb[:]) parse.ToLower(val) if s, ok := ShortenColorHex[string(val)]; ok { if _, err := c.w.Write(s); err != nil { return 0, err } } else { if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] { val[2] = val[3] val[3] = val[5] val = val[:4] } if _, err := c.w.Write(val); err != nil { return 0, err } } return n, nil } } else if fun == css.Hsl && nArgs == 3 { if values[1].TokenType == css.NumberToken && values[3].TokenType == css.PercentageToken && values[5].TokenType == css.PercentageToken { h, err1 := strconv.ParseFloat(string(values[1].Data), 32) s, err2 := strconv.ParseFloat(string(values[3].Data[:len(values[3].Data)-1]), 32) l, err3 := strconv.ParseFloat(string(values[5].Data[:len(values[5].Data)-1]), 32) if err1 == nil && err2 == nil && err3 == nil { r, g, b := css.HSL2RGB(h/360.0, s/100.0, l/100.0) rgb := []byte{byte((r * 255.0) + 0.5), byte((g * 255.0) + 0.5), byte((b * 255.0) + 0.5)} val := make([]byte, 7) val[0] = '#' hex.Encode(val[1:], rgb[:]) parse.ToLower(val) if s, ok := ShortenColorHex[string(val)]; ok { if _, err := c.w.Write(s); err != nil { return 0, err } } else { if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] { val[2] = val[3] val[3] = val[5] val = val[:4] } if _, err := c.w.Write(val); err != nil { return 0, err } } return n, nil } } } } for _, value := range values { if _, err := c.w.Write(value.Data); err != nil { return 0, err } } return n, nil }
func (c *cssMinifier) minifyDeclaration(property []byte, values []css.Token) error { if len(values) == 0 { return nil } prop := css.ToHash(property) inProgid := false for i, value := range values { if inProgid { if value.TokenType == css.FunctionToken { inProgid = false } continue } else if value.TokenType == css.IdentToken && css.ToHash(value.Data) == css.Progid { inProgid = true continue } value.TokenType, value.Data = c.shortenToken(prop, value.TokenType, value.Data) if prop == css.Font || prop == css.Font_Family || prop == css.Font_Weight { if value.TokenType == css.IdentToken && (prop == css.Font || prop == css.Font_Weight) { val := css.ToHash(value.Data) if val == css.Normal && prop == css.Font_Weight { // normal could also be specified for font-variant, not just font-weight value.TokenType = css.NumberToken value.Data = []byte("400") } else if val == css.Bold { value.TokenType = css.NumberToken value.Data = []byte("700") } } else if value.TokenType == css.StringToken && (prop == css.Font || prop == css.Font_Family) && len(value.Data) > 2 { unquote := true parse.ToLower(value.Data) s := value.Data[1 : len(value.Data)-1] if len(s) > 0 { for _, split := range bytes.Split(s, spaceBytes) { val := css.ToHash(split) // if len is zero, it contains two consecutive spaces if val == css.Inherit || val == css.Serif || val == css.Sans_Serif || val == css.Monospace || val == css.Fantasy || val == css.Cursive || val == css.Initial || val == css.Default || len(split) == 0 || !css.IsIdent(split) { unquote = false break } } } if unquote { value.Data = s } } } else if prop == css.Outline || prop == css.Border || prop == css.Border_Bottom || prop == css.Border_Left || prop == css.Border_Right || prop == css.Border_Top { if css.ToHash(value.Data) == css.None { value.TokenType = css.NumberToken value.Data = zeroBytes } } values[i].TokenType, values[i].Data = value.TokenType, value.Data } important := false if len(values) > 2 && values[len(values)-2].TokenType == css.DelimToken && values[len(values)-2].Data[0] == '!' && css.ToHash(values[len(values)-1].Data) == css.Important { values = values[:len(values)-2] important = true } if len(values) == 1 { if prop == css.Background && css.ToHash(values[0].Data) == css.None { values[0].Data = backgroundNoneBytes } else if bytes.Equal(property, msfilterBytes) { alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=") if values[0].TokenType == css.StringToken && bytes.HasPrefix(values[0].Data[1:len(values[0].Data)-1], alpha) { values[0].Data = append(append([]byte{values[0].Data[0]}, []byte("alpha(opacity=")...), values[0].Data[1+len(alpha):]...) } } } else { if prop == css.Margin || prop == css.Padding || prop == css.Border_Width { if (values[0].TokenType == css.NumberToken || values[0].TokenType == css.DimensionToken || values[0].TokenType == css.PercentageToken) && (len(values)+1)%2 == 0 { valid := true for i := 1; i < len(values); i += 2 { if values[i].TokenType != css.WhitespaceToken || values[i+1].TokenType != css.NumberToken && values[i+1].TokenType != css.DimensionToken && values[i+1].TokenType != css.PercentageToken { valid = false break } } if valid { n := (len(values) + 1) / 2 if n == 2 { if bytes.Equal(values[0].Data, values[2].Data) { values = values[:1] } } else if n == 3 { if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[4].Data) { values = values[:1] } else if bytes.Equal(values[0].Data, values[4].Data) { values = values[:3] } } else if n == 4 { if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[4].Data) && bytes.Equal(values[0].Data, values[6].Data) { values = values[:1] } else if bytes.Equal(values[0].Data, values[4].Data) && bytes.Equal(values[2].Data, values[6].Data) { values = values[:3] } else if bytes.Equal(values[2].Data, values[6].Data) { values = values[:5] } } } } } else if prop == css.Filter && len(values) == 11 { if bytes.Equal(values[0].Data, []byte("progid")) && values[1].TokenType == css.ColonToken && bytes.Equal(values[2].Data, []byte("DXImageTransform")) && values[3].Data[0] == '.' && bytes.Equal(values[4].Data, []byte("Microsoft")) && values[5].Data[0] == '.' && bytes.Equal(values[6].Data, []byte("Alpha(")) && bytes.Equal(parse.ToLower(values[7].Data), []byte("opacity")) && values[8].Data[0] == '=' && values[10].Data[0] == ')' { values = values[6:] values[0].Data = []byte("alpha(") } } } for i := 0; i < len(values); i++ { if values[i].TokenType == css.FunctionToken { n, err := c.minifyFunction(values[i:]) if err != nil { return err } i += n - 1 } else if _, err := c.w.Write(values[i].Data); err != nil { return err } } if important { if _, err := c.w.Write([]byte("!important")); err != nil { return err } } return nil }
// Minify minifies SVG data, it reads from r and writes to w. func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]string) error { var tag svg.Hash defaultStyleType := cssMimeBytes defaultStyleParams := map[string]string(nil) defaultInlineStyleParams := map[string]string{"inline": "1"} p := NewPathData(o) minifyBuffer := buffer.NewWriter(make([]byte, 0, 64)) attrByteBuffer := make([]byte, 0, 64) gStack := make([]bool, 0) l := xml.NewLexer(r) tb := NewTokenBuffer(l) for { t := *tb.Shift() SWITCH: switch t.TokenType { case xml.ErrorToken: if l.Err() == io.EOF { return nil } return l.Err() case xml.DOCTYPEToken: if len(t.Text) > 0 && t.Text[len(t.Text)-1] == ']' { if _, err := w.Write(t.Data); err != nil { return err } } case xml.TextToken: t.Data = parse.ReplaceMultipleWhitespace(parse.TrimWhitespace(t.Data)) if tag == svg.Style && len(t.Data) > 0 { if err := m.MinifyMimetype(defaultStyleType, w, buffer.NewReader(t.Data), defaultStyleParams); err != nil { if err != minify.ErrNotExist { return err } else if _, err := w.Write(t.Data); err != nil { return err } } } else if _, err := w.Write(t.Data); err != nil { return err } case xml.CDATAToken: if tag == svg.Style { minifyBuffer.Reset() if err := m.MinifyMimetype(defaultStyleType, minifyBuffer, buffer.NewReader(t.Text), defaultStyleParams); err == nil { t.Data = append(t.Data[:9], minifyBuffer.Bytes()...) t.Text = t.Data[9:] t.Data = append(t.Data, cdataEndBytes...) } else if err != minify.ErrNotExist { return err } } var useText bool if t.Text, useText = xml.EscapeCDATAVal(&attrByteBuffer, t.Text); useText { t.Text = parse.ReplaceMultipleWhitespace(parse.TrimWhitespace(t.Text)) if _, err := w.Write(t.Text); err != nil { return err } } else if _, err := w.Write(t.Data); err != nil { return err } case xml.StartTagPIToken: for { if t := *tb.Shift(); t.TokenType == xml.StartTagClosePIToken || t.TokenType == xml.ErrorToken { break } } case xml.StartTagToken: tag = t.Hash if containerTagMap[tag] { // skip empty containers i := 0 for { next := tb.Peek(i) i++ if next.TokenType == xml.EndTagToken && next.Hash == tag || next.TokenType == xml.StartTagCloseVoidToken || next.TokenType == xml.ErrorToken { for j := 0; j < i; j++ { tb.Shift() } break SWITCH } else if next.TokenType != xml.AttributeToken && next.TokenType != xml.StartTagCloseToken { break } } if tag == svg.G { if tb.Peek(0).TokenType == xml.StartTagCloseToken { gStack = append(gStack, false) tb.Shift() break } gStack = append(gStack, true) } } else if tag == svg.Metadata { skipTag(tb, tag) break } else if tag == svg.Line { o.shortenLine(tb, &t, p) } else if tag == svg.Rect && !o.shortenRect(tb, &t, p) { skipTag(tb, tag) break } else if tag == svg.Polygon || tag == svg.Polyline { o.shortenPoly(tb, &t, p) } if _, err := w.Write(t.Data); err != nil { return err } case xml.AttributeToken: if len(t.AttrVal) == 0 || t.Text == nil { // data is nil when attribute has been removed continue } attr := t.Hash val := t.AttrVal if n, m := parse.Dimension(val); n+m == len(val) { // TODO: inefficient, temporary measure val, _ = o.shortenDimension(val) } if attr == svg.Xml_Space && parse.Equal(val, []byte("preserve")) || tag == svg.Svg && (attr == svg.Version && parse.Equal(val, []byte("1.1")) || attr == svg.X && parse.Equal(val, []byte("0")) || attr == svg.Y && parse.Equal(val, []byte("0")) || attr == svg.Width && parse.Equal(val, []byte("100%")) || attr == svg.Height && parse.Equal(val, []byte("100%")) || attr == svg.PreserveAspectRatio && parse.Equal(val, []byte("xMidYMid meet")) || attr == svg.BaseProfile && parse.Equal(val, []byte("none")) || attr == svg.ContentScriptType && parse.Equal(val, []byte("application/ecmascript")) || attr == svg.ContentStyleType && parse.Equal(val, []byte("text/css"))) || tag == svg.Style && attr == svg.Type && parse.Equal(val, []byte("text/css")) { continue } if _, err := w.Write(spaceBytes); err != nil { return err } if _, err := w.Write(t.Text); err != nil { return err } if _, err := w.Write(isBytes); err != nil { return err } if tag == svg.Svg && attr == svg.ContentStyleType { val = minify.ContentType(val) defaultStyleType = val } else if attr == svg.Style { minifyBuffer.Reset() if err := m.MinifyMimetype(defaultStyleType, minifyBuffer, buffer.NewReader(val), defaultInlineStyleParams); err == nil { val = minifyBuffer.Bytes() } else if err != minify.ErrNotExist { return err } } else if attr == svg.D { val = p.ShortenPathData(val) } else if attr == svg.ViewBox { j := 0 newVal := val[:0] for i := 0; i < 4; i++ { if i != 0 { if j >= len(val) || val[j] != ' ' && val[j] != ',' { newVal = append(newVal, val[j:]...) break } newVal = append(newVal, ' ') j++ } if dim, n := o.shortenDimension(val[j:]); n > 0 { newVal = append(newVal, dim...) j += n } else { newVal = append(newVal, val[j:]...) break } } val = newVal } else if colorAttrMap[attr] && len(val) > 0 { parse.ToLower(val) if val[0] == '#' { if name, ok := minifyCSS.ShortenColorHex[string(val)]; ok { val = name } else if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] { val[2] = val[3] val[3] = val[5] val = val[:4] } } else if hex, ok := minifyCSS.ShortenColorName[css.ToHash(val)]; ok { val = hex } else if len(val) > 5 && parse.Equal(val[:4], []byte("rgb(")) && val[len(val)-1] == ')' { // TODO: handle rgb(x, y, z) and hsl(x, y, z) } } // prefer single or double quotes depending on what occurs more often in value val = xml.EscapeAttrVal(&attrByteBuffer, val) if _, err := w.Write(val); err != nil { return err } case xml.StartTagCloseToken: next := tb.Peek(0) skipExtra := false if next.TokenType == xml.TextToken && parse.IsAllWhitespace(next.Data) { next = tb.Peek(1) skipExtra = true } if next.TokenType == xml.EndTagToken { // collapse empty tags to single void tag tb.Shift() if skipExtra { tb.Shift() } if _, err := w.Write(voidBytes); err != nil { return err } } else { if _, err := w.Write(t.Data); err != nil { return err } } case xml.StartTagCloseVoidToken: tag = 0 if _, err := w.Write(t.Data); err != nil { return err } case xml.EndTagToken: tag = 0 if t.Hash == svg.G && len(gStack) > 0 { if !gStack[len(gStack)-1] { gStack = gStack[:len(gStack)-1] break } gStack = gStack[:len(gStack)-1] } if len(t.Data) > 3+len(t.Text) { t.Data[2+len(t.Text)] = '>' t.Data = t.Data[:3+len(t.Text)] } if _, err := w.Write(t.Data); err != nil { return err } } } }
// Minify minifies HTML data, it reads from r and writes to w. func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]string) error { var rawTagHash html.Hash var rawTagTraits traits var rawTagMediatype []byte omitSpace := true // if true the next leading space is omitted defaultScriptType := jsMimeBytes defaultScriptParams := map[string]string(nil) defaultStyleType := cssMimeBytes defaultStyleParams := map[string]string(nil) defaultInlineStyleParams := map[string]string{"inline": "1"} attrMinifyBuffer := buffer.NewWriter(make([]byte, 0, 64)) attrByteBuffer := make([]byte, 0, 64) attrTokenBuffer := make([]*Token, 0, maxAttrLookup) l := html.NewLexer(r) tb := NewTokenBuffer(l) for { t := *tb.Shift() SWITCH: switch t.TokenType { case html.ErrorToken: if l.Err() == io.EOF { return nil } return l.Err() case html.DoctypeToken: if _, err := w.Write([]byte("<!doctype html>")); err != nil { return err } case html.CommentToken: // TODO: ensure that nested comments are handled properly (lexer doesn't handle this!) var comment []byte if bytes.HasPrefix(t.Data, []byte("[if")) { comment = append(append([]byte("<!--"), t.Data...), []byte("-->")...) } else if bytes.HasSuffix(t.Data, []byte("--")) { // only occurs when mixed up with conditional comments comment = append(append([]byte("<!"), t.Data...), '>') } if _, err := w.Write(comment); err != nil { return err } case html.TextToken: // CSS and JS minifiers for inline code if rawTagHash != 0 { if rawTagHash == html.Style || rawTagHash == html.Script || rawTagHash == html.Iframe || rawTagHash == html.Svg || rawTagHash == html.Math { var mimetype []byte var params map[string]string if rawTagHash == html.Iframe { mimetype = htmlMimeBytes } else if rawTagHash == html.Svg { mimetype = svgMimeBytes } else if rawTagHash == html.Math { mimetype = mathMimeBytes } else if len(rawTagMediatype) > 0 { mimetype, params = parse.Mediatype(rawTagMediatype) } else if rawTagHash == html.Script { mimetype = defaultScriptType params = defaultScriptParams } else if rawTagHash == html.Style { mimetype = defaultStyleType params = defaultStyleParams } // TODO: really necessary? // ignore CDATA because that only has meaning in XML if trimmedData := parse.TrimWhitespace(t.Data); len(trimmedData) > 12 && bytes.Equal(trimmedData[:9], cdataBytes) && bytes.Equal(trimmedData[len(trimmedData)-3:], cdataEndBytes) { t.Data = trimmedData[9 : len(trimmedData)-3] } if err := m.MinifyMimetype(mimetype, w, buffer.NewReader(t.Data), params); err != nil { if _, err := w.Write(t.Data); err != nil { return err } } } else if _, err := w.Write(t.Data); err != nil { return err } if rawTagTraits&nonPhrasingTag == 0 && rawTagHash != html.Script { omitSpace = len(t.Data) > 0 && t.Data[len(t.Data)-1] == ' ' } } else { t.Data = parse.ReplaceMultipleWhitespace(t.Data) if !o.KeepWhitespace { // whitespace removal; trim left if omitSpace && t.Data[0] == ' ' { t.Data = t.Data[1:] } // whitespace removal; trim right omitSpace = false if len(t.Data) == 0 { omitSpace = true } else if t.Data[len(t.Data)-1] == ' ' { omitSpace = true i := 0 for { next := tb.Peek(i) // trim if EOF, text token with leading whitespace or block token if next.TokenType == html.ErrorToken { t.Data = t.Data[:len(t.Data)-1] omitSpace = false break } else if next.TokenType == html.TextToken { // remove if the text token starts with a whitespace if len(next.Data) > 0 && parse.IsWhitespace(next.Data[0]) { t.Data = t.Data[:len(t.Data)-1] omitSpace = false } break } else if next.TokenType == html.StartTagToken || next.TokenType == html.EndTagToken { // remove when followed up by a block tag if next.Traits&nonPhrasingTag != 0 { t.Data = t.Data[:len(t.Data)-1] omitSpace = false break } else if next.TokenType == html.StartTagToken { break } } i++ } } } if _, err := w.Write(t.Data); err != nil { return err } } case html.StartTagToken, html.EndTagToken: rawTagHash = 0 hasAttributes := false if t.TokenType == html.StartTagToken { if next := tb.Peek(0); next.TokenType == html.AttributeToken { hasAttributes = true } if t.Traits&rawTag != 0 { // ignore empty script and style tags if !hasAttributes && (t.Hash == html.Script || t.Hash == html.Style) { if next := tb.Peek(1); next.TokenType == html.EndTagToken { tb.Shift() tb.Shift() break } } rawTagHash = t.Hash rawTagTraits = t.Traits rawTagMediatype = nil } } if t.Traits&nonPhrasingTag != 0 { omitSpace = true // omit spaces after block elements } // remove superfluous ending tags if !hasAttributes && (t.Hash == html.Html || t.Hash == html.Head || t.Hash == html.Body || t.Hash == html.Colgroup) { break } else if t.TokenType == html.EndTagToken { if t.Hash == html.Thead || t.Hash == html.Tbody || t.Hash == html.Tfoot || t.Hash == html.Tr || t.Hash == html.Th || t.Hash == html.Td || t.Hash == html.Optgroup || t.Hash == html.Option || t.Hash == html.Dd || t.Hash == html.Dt || t.Hash == html.Li || t.Hash == html.Rb || t.Hash == html.Rt || t.Hash == html.Rtc || t.Hash == html.Rp { break } else if t.Hash == html.P { i := 0 for { next := tb.Peek(i) i++ // continue if text token is empty or whitespace if next.TokenType == html.TextToken && parse.IsAllWhitespace(next.Data) { continue } if next.TokenType == html.ErrorToken || next.TokenType == html.EndTagToken && next.Hash != html.A || next.TokenType == html.StartTagToken && next.Traits&nonPhrasingTag != 0 { break SWITCH } break } } } // write tag if t.TokenType == html.EndTagToken { if _, err := w.Write(endBytes); err != nil { return err } } else { if _, err := w.Write(ltBytes); err != nil { return err } } if _, err := w.Write(t.Data); err != nil { return err } if hasAttributes { // rewrite attributes with interdependent conditions if t.Hash == html.A { getAttributes(&attrTokenBuffer, tb, html.Id, html.Name, html.Rel, html.Href) if id := attrTokenBuffer[0]; id != nil { if name := attrTokenBuffer[1]; name != nil && parse.Equal(id.AttrVal, name.AttrVal) { name.Data = nil } } if href := attrTokenBuffer[3]; href != nil { if len(href.AttrVal) > 5 && parse.EqualFold(href.AttrVal[:4], httpBytes) { if href.AttrVal[4] == ':' { if m.URL != nil && m.URL.Scheme == "http" { href.AttrVal = href.AttrVal[5:] } else { parse.ToLower(href.AttrVal[:4]) } } else if (href.AttrVal[4] == 's' || href.AttrVal[4] == 'S') && href.AttrVal[5] == ':' { if m.URL != nil && m.URL.Scheme == "https" { href.AttrVal = href.AttrVal[6:] } else { parse.ToLower(href.AttrVal[:5]) } } } } } else if t.Hash == html.Meta { getAttributes(&attrTokenBuffer, tb, html.Content, html.Http_Equiv, html.Charset, html.Name) if content := attrTokenBuffer[0]; content != nil { if httpEquiv := attrTokenBuffer[1]; httpEquiv != nil { content.AttrVal = minify.ContentType(content.AttrVal) if charset := attrTokenBuffer[2]; charset == nil && parse.EqualFold(httpEquiv.AttrVal, []byte("content-type")) && parse.Equal(content.AttrVal, []byte("text/html;charset=utf-8")) { httpEquiv.Data = nil content.Data = []byte("charset") content.Hash = html.Charset content.AttrVal = []byte("utf-8") } else if parse.EqualFold(httpEquiv.AttrVal, []byte("content-style-type")) { defaultStyleType, defaultStyleParams = parse.Mediatype(content.AttrVal) if defaultStyleParams != nil { defaultInlineStyleParams = defaultStyleParams defaultInlineStyleParams["inline"] = "1" } else { defaultInlineStyleParams = map[string]string{"inline": "1"} } } else if parse.EqualFold(httpEquiv.AttrVal, []byte("content-script-type")) { defaultScriptType, defaultScriptParams = parse.Mediatype(content.AttrVal) } } if name := attrTokenBuffer[3]; name != nil { if parse.EqualFold(name.AttrVal, []byte("keywords")) { content.AttrVal = bytes.Replace(content.AttrVal, []byte(", "), []byte(","), -1) } else if parse.EqualFold(name.AttrVal, []byte("viewport")) { content.AttrVal = bytes.Replace(content.AttrVal, []byte(" "), []byte(""), -1) } } } } else if t.Hash == html.Script { getAttributes(&attrTokenBuffer, tb, html.Src, html.Charset) if src := attrTokenBuffer[0]; src != nil { if charset := attrTokenBuffer[1]; charset != nil { charset.Data = nil } } } // write attributes for { attr := *tb.Shift() if attr.TokenType != html.AttributeToken { break } else if attr.Data == nil { continue // removed attribute } val := attr.AttrVal if len(val) > 1 && (val[0] == '"' || val[0] == '\'') { val = parse.TrimWhitespace(val[1 : len(val)-1]) } if len(val) == 0 && (attr.Hash == html.Class || attr.Hash == html.Dir || attr.Hash == html.Id || attr.Hash == html.Lang || attr.Hash == html.Name || attr.Hash == html.Title || attr.Hash == html.Action && t.Hash == html.Form || attr.Hash == html.Value && t.Hash == html.Input) { continue // omit empty attribute values } if attr.Traits&caselessAttr != 0 { val = parse.ToLower(val) if attr.Hash == html.Enctype || attr.Hash == html.Codetype || attr.Hash == html.Accept || attr.Hash == html.Type && (t.Hash == html.A || t.Hash == html.Link || t.Hash == html.Object || t.Hash == html.Param || t.Hash == html.Script || t.Hash == html.Style || t.Hash == html.Source) { val = minify.ContentType(val) } } if rawTagHash != 0 && attr.Hash == html.Type { rawTagMediatype = val } // default attribute values can be ommited if !o.KeepDefaultAttrVals && (attr.Hash == html.Type && (t.Hash == html.Script && parse.Equal(val, []byte("text/javascript")) || t.Hash == html.Style && parse.Equal(val, []byte("text/css")) || t.Hash == html.Link && parse.Equal(val, []byte("text/css")) || t.Hash == html.Input && parse.Equal(val, []byte("text")) || t.Hash == html.Button && parse.Equal(val, []byte("submit"))) || attr.Hash == html.Language && t.Hash == html.Script || attr.Hash == html.Method && parse.Equal(val, []byte("get")) || attr.Hash == html.Enctype && parse.Equal(val, []byte("application/x-www-form-urlencoded")) || attr.Hash == html.Colspan && parse.Equal(val, []byte("1")) || attr.Hash == html.Rowspan && parse.Equal(val, []byte("1")) || attr.Hash == html.Shape && parse.Equal(val, []byte("rect")) || attr.Hash == html.Span && parse.Equal(val, []byte("1")) || attr.Hash == html.Clear && parse.Equal(val, []byte("none")) || attr.Hash == html.Frameborder && parse.Equal(val, []byte("1")) || attr.Hash == html.Scrolling && parse.Equal(val, []byte("auto")) || attr.Hash == html.Valuetype && parse.Equal(val, []byte("data")) || attr.Hash == html.Media && t.Hash == html.Style && parse.Equal(val, []byte("all"))) { continue } // CSS and JS minifiers for attribute inline code if attr.Hash == html.Style { attrMinifyBuffer.Reset() if m.MinifyMimetype(defaultStyleType, attrMinifyBuffer, buffer.NewReader(val), defaultInlineStyleParams) == nil { val = attrMinifyBuffer.Bytes() } if len(val) == 0 { continue } } else if len(attr.Data) > 2 && attr.Data[0] == 'o' && attr.Data[1] == 'n' { if len(val) >= 11 && parse.EqualFold(val[:11], jsSchemeBytes) { val = val[11:] } attrMinifyBuffer.Reset() if m.MinifyMimetype(defaultScriptType, attrMinifyBuffer, buffer.NewReader(val), defaultScriptParams) == nil { val = attrMinifyBuffer.Bytes() } if len(val) == 0 { continue } } else if len(val) > 5 && attr.Traits&urlAttr != 0 { // anchors are already handled if t.Hash != html.A { if parse.EqualFold(val[:4], httpBytes) { if val[4] == ':' { if m.URL != nil && m.URL.Scheme == "http" { val = val[5:] } else { parse.ToLower(val[:4]) } } else if (val[4] == 's' || val[4] == 'S') && val[5] == ':' { if m.URL != nil && m.URL.Scheme == "https" { val = val[6:] } else { parse.ToLower(val[:5]) } } } } if parse.EqualFold(val[:5], dataSchemeBytes) { val = minify.DataURI(m, val) } } if _, err := w.Write(spaceBytes); err != nil { return err } if _, err := w.Write(attr.Data); err != nil { return err } if len(val) > 0 && attr.Traits&booleanAttr == 0 { if _, err := w.Write(isBytes); err != nil { return err } // no quotes if possible, else prefer single or double depending on which occurs more often in value val = html.EscapeAttrVal(&attrByteBuffer, attr.AttrVal, val) if _, err := w.Write(val); err != nil { return err } } } } if _, err := w.Write(gtBytes); err != nil { return err } } } }
// Minify minifies SVG data, it reads from r and writes to w. func Minify(m minify.Minifier, _ string, w io.Writer, r io.Reader) error { var tag svg.Hash attrMinifyBuffer := buffer.NewWriter(make([]byte, 0, 64)) attrByteBuffer := make([]byte, 0, 64) l := xml.NewLexer(r) tb := xml.NewTokenBuffer(l) for { t := *tb.Shift() if t.TokenType == xml.CDATAToken { var useCDATA bool if t.Data, useCDATA = xml.EscapeCDATAVal(&attrByteBuffer, t.Data); !useCDATA { t.TokenType = xml.TextToken } } SWITCH: switch t.TokenType { case xml.ErrorToken: if l.Err() == io.EOF { return nil } return l.Err() case xml.TextToken: t.Data = parse.ReplaceMultiple(parse.Trim(t.Data, parse.IsWhitespace), parse.IsWhitespace, ' ') if tag == svg.Style && len(t.Data) > 0 { if err := m.Minify("text/css", w, buffer.NewReader(t.Data)); err != nil { if err == minify.ErrNotExist { // no minifier, write the original if _, err := w.Write(t.Data); err != nil { return err } } else { return err } } } else if _, err := w.Write(t.Data); err != nil { return err } case xml.CDATAToken: if _, err := w.Write(cdataStartBytes); err != nil { return err } t.Data = parse.ReplaceMultiple(parse.Trim(t.Data, parse.IsWhitespace), parse.IsWhitespace, ' ') if tag == svg.Style && len(t.Data) > 0 { if err := m.Minify("text/css", w, buffer.NewReader(t.Data)); err != nil { if err == minify.ErrNotExist { // no minifier, write the original if _, err := w.Write(t.Data); err != nil { return err } } else { return err } } } else if _, err := w.Write(t.Data); err != nil { return err } if _, err := w.Write(cdataEndBytes); err != nil { return err } case xml.StartTagPIToken: for { if t := *tb.Shift(); t.TokenType == xml.StartTagClosePIToken || t.TokenType == xml.ErrorToken { break } } case xml.StartTagToken: tag = svg.ToHash(t.Data) if containerTagMap[tag] { // skip empty containers i := 0 for { next := tb.Peek(i) i++ if next.TokenType == xml.EndTagToken && svg.ToHash(next.Data) == tag || next.TokenType == xml.StartTagCloseVoidToken || next.TokenType == xml.ErrorToken { for j := 0; j < i; j++ { tb.Shift() } break SWITCH } else if next.TokenType != xml.AttributeToken && next.TokenType != xml.StartTagCloseToken { break } } } else if tag == svg.Metadata { for { if t := *tb.Shift(); t.TokenType == xml.EndTagToken && svg.ToHash(t.Data) == tag || t.TokenType == xml.StartTagCloseVoidToken || t.TokenType == xml.ErrorToken { break } } break } else if tag == svg.Line || tag == svg.Rect { // TODO: shape2path also for polygon and polyline // x1, y1, x2, y2 float64 := 0, 0, 0, 0 // valid := true // i := 0 // for { // next := tb.Peek(i) // i++ // if next.TokenType != xml.AttributeToken { // break // } // v *int // attr := svg.ToHash(next.Data) // if tag == svg.Line { // if attr == svg.X1 { // v = &x1 // } else if attr == svg.Y1 { // v = &y1 // } else if attr == svg.X2 { // v = &x2 // } else if attr == svg.Y2 { // v = &Y2 // } else { // continue // } // } else if attr == svg.X { // rect // v = &x1 // } else if attr == svg.Y { // v = &y1 // } else if attr == svg.Width { // v = &x2 // } else if attr == svg.Height { // v = &Y2 // } else if attr == svg.Rx || attr == svg.Ry { // valid = false // break // } else { // continue // } // } // if valid { // t.Data = pathBytes // } } if _, err := w.Write(ltBytes); err != nil { return err } if _, err := w.Write(t.Data); err != nil { return err } case xml.AttributeToken: if len(t.AttrVal) < 2 { continue } attr := svg.ToHash(t.Data) val := parse.ReplaceMultiple(parse.Trim(t.AttrVal[1:len(t.AttrVal)-1], parse.IsWhitespace), parse.IsWhitespace, ' ') if tag == svg.Svg && attr == svg.Version { continue } if _, err := w.Write(spaceBytes); err != nil { return err } if _, err := w.Write(t.Data); err != nil { return err } if _, err := w.Write(isBytes); err != nil { return err } if attr == svg.Style { attrMinifyBuffer.Reset() if m.Minify("text/css;inline=1", attrMinifyBuffer, buffer.NewReader(val)) == nil { val = attrMinifyBuffer.Bytes() } } else if attr == svg.D { val = shortenPathData(val) } else if attr == svg.ViewBox { j := 0 newVal := val[:0] for i := 0; i < 4; i++ { if i != 0 { if j >= len(val) || val[j] != ' ' && val[j] != ',' { newVal = append(newVal, val[j:]...) break } newVal = append(newVal, ' ') j++ } if dim, n := shortenDimension(val[j:]); n > 0 { newVal = append(newVal, dim...) j += n } else { newVal = append(newVal, val[j:]...) break } } val = newVal } else if colorAttrMap[attr] && len(val) > 0 { parse.ToLower(val) if val[0] == '#' { if name, ok := minifyCSS.ShortenColorHex[string(val)]; ok { val = name } else if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] { val[2] = val[3] val[3] = val[5] val = val[:4] } } else if hex, ok := minifyCSS.ShortenColorName[css.ToHash(val)]; ok { val = hex } else if len(val) > 5 && parse.Equal(val[:4], []byte("rgb(")) && val[len(val)-1] == ')' { // TODO: handle rgb(x, y, z) and hsl(x, y, z) } } else if dim, n := shortenDimension(val); n == len(val) { val = dim } // prefer single or double quotes depending on what occurs more often in value val = xml.EscapeAttrVal(&attrByteBuffer, val) if _, err := w.Write(val); err != nil { return err } case xml.StartTagCloseToken: next := tb.Peek(0) skipExtra := false if next.TokenType == xml.TextToken && parse.IsAllWhitespace(next.Data) { next = tb.Peek(1) skipExtra = true } if next.TokenType == xml.EndTagToken { // collapse empty tags to single void tag tb.Shift() if skipExtra { tb.Shift() } if _, err := w.Write(voidBytes); err != nil { return err } } else { if _, err := w.Write(gtBytes); err != nil { return err } } case xml.StartTagCloseVoidToken: if _, err := w.Write(voidBytes); err != nil { return err } case xml.EndTagToken: if _, err := w.Write(endBytes); err != nil { return err } if _, err := w.Write(t.Data); err != nil { return err } if _, err := w.Write(gtBytes); err != nil { return err } } } }
// Minify minifies HTML data, it reads from r and writes to w. func Minify(m minify.Minifier, _ string, w io.Writer, r io.Reader) error { var rawTag html.Hash var rawTagMediatype []byte omitSpace := true // if true the next leading space is omitted defaultScriptType := "text/javascript" defaultStyleType := "text/css" defaultInlineStyleType := "text/css;inline=1" attrMinifyBuffer := buffer.NewWriter(make([]byte, 0, 64)) attrByteBuffer := make([]byte, 0, 64) attrIntBuffer := make([]int, 0, maxAttrLookup) attrTokenBuffer := make([]*html.Token, 0, maxAttrLookup) l := html.NewLexer(r) tb := html.NewTokenBuffer(l) for { t := *tb.Shift() SWITCH: switch t.TokenType { case html.ErrorToken: if l.Err() == io.EOF { return nil } return l.Err() case html.DoctypeToken: if _, err := w.Write([]byte("<!doctype html>")); err != nil { return err } case html.CommentToken: // TODO: ensure that nested comments are handled properly (lexer doesn't handle this!) var comment []byte if bytes.HasPrefix(t.Data, []byte("[if")) { comment = append(append([]byte("<!--"), t.Data...), []byte("-->")...) } else if bytes.HasSuffix(t.Data, []byte("--")) { // only occurs when mixed up with conditional comments comment = append(append([]byte("<!"), t.Data...), '>') } if _, err := w.Write(comment); err != nil { return err } case html.TextToken: // CSS and JS minifiers for inline code if rawTag != 0 { if rawTag == html.Style || rawTag == html.Script || rawTag == html.Iframe || rawTag == html.Svg || rawTag == html.Math { var mediatype string if rawTag == html.Iframe { mediatype = "text/html" } else if len(rawTagMediatype) > 0 { mediatype = string(rawTagMediatype) } else if rawTag == html.Script { mediatype = defaultScriptType } else if rawTag == html.Style { mediatype = defaultStyleType } else if rawTag == html.Svg { mediatype = "image/svg+xml" } else if rawTag == html.Math { mediatype = "application/mathml+xml" } // ignore CDATA if trimmedData := parse.Trim(t.Data, parse.IsWhitespace); len(trimmedData) > 12 && bytes.Equal(trimmedData[:9], []byte("<![CDATA[")) && bytes.Equal(trimmedData[len(trimmedData)-3:], []byte("]]>")) { t.Data = trimmedData[9 : len(trimmedData)-3] } if err := m.Minify(mediatype, w, buffer.NewReader(t.Data)); err != nil { if _, err := w.Write(t.Data); err != nil { return err } } } else if _, err := w.Write(t.Data); err != nil { return err } if !nonPhrasingTagMap[rawTag] && rawTag != html.Script { omitSpace = len(t.Data) > 0 && t.Data[len(t.Data)-1] == ' ' } } else { t.Data = parse.ReplaceMultiple(t.Data, parse.IsWhitespace, ' ') // whitespace removal; trim left if omitSpace && t.Data[0] == ' ' { t.Data = t.Data[1:] } // whitespace removal; trim right omitSpace = false if len(t.Data) == 0 { omitSpace = true } else if t.Data[len(t.Data)-1] == ' ' { omitSpace = true i := 0 for { next := tb.Peek(i) // trim if EOF, text token with leading whitespace or block token if next.TokenType == html.ErrorToken { t.Data = t.Data[:len(t.Data)-1] omitSpace = false break } else if next.TokenType == html.TextToken { // remove if the text token starts with a whitespace if len(next.Data) > 0 && parse.IsWhitespace(next.Data[0]) { t.Data = t.Data[:len(t.Data)-1] omitSpace = false } break } else if next.TokenType == html.StartTagToken || next.TokenType == html.EndTagToken { // remove when followed up by a block tag if nonPhrasingTagMap[next.Hash] { t.Data = t.Data[:len(t.Data)-1] omitSpace = false break } else if next.TokenType == html.StartTagToken { break } } i++ } } if _, err := w.Write(t.Data); err != nil { return err } } case html.StartTagToken, html.EndTagToken: rawTag = 0 hasAttributes := false if t.TokenType == html.StartTagToken { if next := tb.Peek(0); next.TokenType == html.AttributeToken { hasAttributes = true } if rawTagMap[t.Hash] { // ignore empty script and style tags if !hasAttributes && (t.Hash == html.Script || t.Hash == html.Style) { if next := tb.Peek(1); next.TokenType == html.EndTagToken { tb.Shift() tb.Shift() break } } rawTag = t.Hash rawTagMediatype = []byte{} } } if nonPhrasingTagMap[t.Hash] { omitSpace = true // omit spaces after block elements } // remove superfluous ending tags if !hasAttributes && (t.Hash == html.Html || t.Hash == html.Head || t.Hash == html.Body || t.Hash == html.Colgroup) { break } else if t.TokenType == html.EndTagToken { if t.Hash == html.Thead || t.Hash == html.Tbody || t.Hash == html.Tfoot || t.Hash == html.Tr || t.Hash == html.Th || t.Hash == html.Td || t.Hash == html.Optgroup || t.Hash == html.Option || t.Hash == html.Dd || t.Hash == html.Dt || t.Hash == html.Li || t.Hash == html.Rb || t.Hash == html.Rt || t.Hash == html.Rtc || t.Hash == html.Rp { break } else if t.Hash == html.P { i := 0 for { next := tb.Peek(i) i++ // continue if text token is empty or whitespace if next.TokenType == html.TextToken && parse.IsAllWhitespace(next.Data) { continue } if next.TokenType == html.ErrorToken || next.TokenType == html.EndTagToken && next.Hash != html.A || next.TokenType == html.StartTagToken && nonPhrasingTagMap[next.Hash] { break SWITCH } break } } } // write tag if t.TokenType == html.EndTagToken { if _, err := w.Write(endBytes); err != nil { return err } } else { if _, err := w.Write(ltBytes); err != nil { return err } } if _, err := w.Write(t.Data); err != nil { return err } if hasAttributes { // rewrite attributes with interdependent conditions if t.Hash == html.A { if attr := getAttributes(tb, &attrIntBuffer, &attrTokenBuffer, html.Id, html.Name, html.Rel, html.Href); attr != nil { if id := attr[0]; id != nil { if name := attr[1]; name != nil && parse.Equal(id.AttrVal, name.AttrVal) { name.Data = nil } } // TODO: omit http or https according to URL, specified through options // if rel := attr[2]; rel == nil || !parse.EqualFold(rel.AttrVal, externalBytes) { // if href := attr[3]; href != nil { // if len(href.AttrVal) > 5 && parse.EqualFold(href.AttrVal[:4], []byte{'h', 't', 't', 'p'}) { // if href.AttrVal[4] == ':' { // href.AttrVal = href.AttrVal[5:] // } else if (href.AttrVal[4] == 's' || href.AttrVal[4] == 'S') && href.AttrVal[5] == ':' { // href.AttrVal = href.AttrVal[6:] // } // } // } // } } } else if t.Hash == html.Meta { if attr := getAttributes(tb, &attrIntBuffer, &attrTokenBuffer, html.Content, html.Http_Equiv, html.Charset, html.Name); attr != nil { if content := attr[0]; content != nil { if httpEquiv := attr[1]; httpEquiv != nil { content.AttrVal = minify.ContentType(content.AttrVal) if charset := attr[2]; charset == nil && parse.EqualFold(httpEquiv.AttrVal, []byte("content-type")) && parse.Equal(content.AttrVal, []byte("text/html;charset=utf-8")) { httpEquiv.Data = nil content.Data = []byte("charset") content.Hash = html.Charset content.AttrVal = []byte("utf-8") } else if parse.EqualFold(httpEquiv.AttrVal, []byte("content-style-type")) { defaultStyleType = string(content.AttrVal) defaultInlineStyleType = defaultStyleType + ";inline=1" } else if parse.EqualFold(httpEquiv.AttrVal, []byte("content-script-type")) { defaultScriptType = string(content.AttrVal) } } if name := attr[3]; name != nil { if parse.EqualFold(name.AttrVal, []byte("keywords")) { content.AttrVal = bytes.Replace(content.AttrVal, []byte(", "), []byte(","), -1) } else if parse.EqualFold(name.AttrVal, []byte("viewport")) { content.AttrVal = bytes.Replace(content.AttrVal, []byte(" "), []byte(""), -1) } } } } } else if t.Hash == html.Script { if attr := getAttributes(tb, &attrIntBuffer, &attrTokenBuffer, html.Src, html.Charset); attr != nil { if src := attr[0]; src != nil { if charset := attr[1]; charset != nil { charset.Data = nil } } } } // write attributes for { attr := *tb.Shift() if attr.TokenType != html.AttributeToken { break } else if attr.Data == nil { continue // removed attribute } val := attr.AttrVal if len(val) > 1 && (val[0] == '"' || val[0] == '\'') { val = parse.Trim(val[1:len(val)-1], parse.IsWhitespace) } if len(val) == 0 && (attr.Hash == html.Class || attr.Hash == html.Dir || attr.Hash == html.Id || attr.Hash == html.Lang || attr.Hash == html.Name || attr.Hash == html.Title || attr.Hash == html.Action && t.Hash == html.Form || attr.Hash == html.Value && t.Hash == html.Input) { continue // omit empty attribute values } if caseInsensitiveAttrMap[attr.Hash] { val = parse.ToLower(val) if attr.Hash == html.Enctype || attr.Hash == html.Codetype || attr.Hash == html.Accept || attr.Hash == html.Type && (t.Hash == html.A || t.Hash == html.Link || t.Hash == html.Object || t.Hash == html.Param || t.Hash == html.Script || t.Hash == html.Style || t.Hash == html.Source) { val = minify.ContentType(val) } } if rawTag != 0 && attr.Hash == html.Type { rawTagMediatype = val } // default attribute values can be ommited if attr.Hash == html.Type && (t.Hash == html.Script && parse.Equal(val, []byte("text/javascript")) || t.Hash == html.Style && parse.Equal(val, []byte("text/css")) || t.Hash == html.Link && parse.Equal(val, []byte("text/css")) || t.Hash == html.Input && parse.Equal(val, []byte("text")) || t.Hash == html.Button && parse.Equal(val, []byte("submit"))) || attr.Hash == html.Language && t.Hash == html.Script || attr.Hash == html.Method && parse.Equal(val, []byte("get")) || attr.Hash == html.Enctype && parse.Equal(val, []byte("application/x-www-form-urlencoded")) || attr.Hash == html.Colspan && parse.Equal(val, []byte("1")) || attr.Hash == html.Rowspan && parse.Equal(val, []byte("1")) || attr.Hash == html.Shape && parse.Equal(val, []byte("rect")) || attr.Hash == html.Span && parse.Equal(val, []byte("1")) || attr.Hash == html.Clear && parse.Equal(val, []byte("none")) || attr.Hash == html.Frameborder && parse.Equal(val, []byte("1")) || attr.Hash == html.Scrolling && parse.Equal(val, []byte("auto")) || attr.Hash == html.Valuetype && parse.Equal(val, []byte("data")) || attr.Hash == html.Media && t.Hash == html.Style && parse.Equal(val, []byte("all")) { continue } // CSS and JS minifiers for attribute inline code if attr.Hash == html.Style { attrMinifyBuffer.Reset() if m.Minify(defaultInlineStyleType, attrMinifyBuffer, buffer.NewReader(val)) == nil { val = attrMinifyBuffer.Bytes() } if len(val) == 0 { continue } } else if len(attr.Data) > 2 && attr.Data[0] == 'o' && attr.Data[1] == 'n' { if len(val) >= 11 && parse.EqualFold(val[:11], []byte("javascript:")) { val = val[11:] } attrMinifyBuffer.Reset() if m.Minify(defaultScriptType, attrMinifyBuffer, buffer.NewReader(val)) == nil { val = attrMinifyBuffer.Bytes() } if len(val) == 0 { continue } } else if len(val) > 5 && urlAttrMap[attr.Hash] { // anchors are already handled // TODO: omit http or https according to URL, specified through options //if t.Hash != html.A { // if parse.EqualFold(val[:4], []byte{'h', 't', 't', 'p'}) { // if val[4] == ':' { // val = val[5:] // } else if (val[4] == 's' || val[4] == 'S') && val[5] == ':' { // val = val[6:] // } // } // } else if parse.EqualFold(val[:5], []byte{'d', 'a', 't', 'a', ':'}) { val = minify.DataURI(m, val) } } if _, err := w.Write(spaceBytes); err != nil { return err } if _, err := w.Write(attr.Data); err != nil { return err } if len(val) > 0 && !booleanAttrMap[attr.Hash] { if _, err := w.Write(isBytes); err != nil { return err } // no quotes if possible, else prefer single or double depending on which occurs more often in value val = html.EscapeAttrVal(&attrByteBuffer, attr.AttrVal, val) if _, err := w.Write(val); err != nil { return err } } } } if _, err := w.Write(gtBytes); err != nil { return err } } } }
func (l *Lexer) shiftRawText() []byte { if l.rawTag == Plaintext { for { if l.r.Peek(0) == 0 { return l.r.Shift() } l.r.Move(1) } } else { // RCDATA, RAWTEXT and SCRIPT for { c := l.r.Peek(0) if c == '<' { if l.r.Peek(1) == '/' { mark := l.r.Pos() l.r.Move(2) for { if c = l.r.Peek(0); !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { break } l.r.Move(1) } if h := ToHash(parse.ToLower(parse.Copy(l.r.Lexeme()[mark+2:]))); h == l.rawTag { // copy so that ToLower doesn't change the case of the underlying slice l.r.Rewind(mark) return l.r.Shift() } } else if l.rawTag == Script && l.r.Peek(1) == '!' && l.r.Peek(2) == '-' && l.r.Peek(3) == '-' { l.r.Move(4) inScript := false for { c := l.r.Peek(0) if c == '-' && l.r.Peek(1) == '-' && l.r.Peek(2) == '>' { l.r.Move(3) break } else if c == '<' { isEnd := l.r.Peek(1) == '/' if isEnd { l.r.Move(2) } else { l.r.Move(1) } mark := l.r.Pos() for { if c = l.r.Peek(0); !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { break } l.r.Move(1) } if h := ToHash(parse.ToLower(parse.Copy(l.r.Lexeme()[mark:]))); h == Script { // copy so that ToLower doesn't change the case of the underlying slice if !isEnd { inScript = true } else { if !inScript { l.r.Rewind(mark - 2) return l.r.Shift() } inScript = false } } } else if c == 0 { return l.r.Shift() } l.r.Move(1) } } else { l.r.Move(1) } } else if c == 0 { return l.r.Shift() } else { l.r.Move(1) } } } }