func TestBuffer(t *testing.T) { // 0 12 3 4 5 6 7 8 9 01 s := `<svg><path d="M0 0L1 1z"/>text<tag/>text</svg>` z := NewTokenBuffer(xml.NewLexer(bytes.NewBufferString(s))) tok := z.Shift() test.That(t, tok.Hash == svg.Svg, "first token is <svg>") test.That(t, z.pos == 0, "shift first token and restore position") test.That(t, len(z.buf) == 0, "shift first token and restore length") test.That(t, z.Peek(2).Hash == svg.D, "third token is d") test.That(t, z.pos == 0, "don't change positon after peeking") test.That(t, len(z.buf) == 3, "mtwo tokens after peeking") test.That(t, z.Peek(8).Hash == svg.Svg, "nineth token is <svg>") test.That(t, z.pos == 0, "don't change positon after peeking") test.That(t, len(z.buf) == 9, "nine tokens after peeking") test.That(t, z.Peek(9).TokenType == xml.ErrorToken, "tenth token is an error") test.That(t, z.Peek(9) == z.Peek(10), "tenth and eleventh token are EOF") test.That(t, len(z.buf) == 10, "ten tokens after peeking") tok = z.Shift() tok = z.Shift() test.That(t, tok.Hash == svg.Path, "third token is <path>") test.That(t, z.pos == 2, "don't change positon after peeking") }
func TestBuffer(t *testing.T) { // 0 12 3 4 5 6 7 8 9 01 s := `<svg><path d="M0 0L1 1z"/>text<tag/>text</svg>` z := NewTokenBuffer(xml.NewLexer(bytes.NewBufferString(s))) tok := z.Shift() assert.Equal(t, svg.Svg, tok.Hash, "first token must be <svg>") assert.Equal(t, 0, z.pos, "must have shifted first token and restored position") assert.Equal(t, 0, len(z.buf), "must have shifted first token and restored length") assert.Equal(t, svg.D, z.Peek(2).Hash, "third token must be d") assert.Equal(t, 0, z.pos, "must not have changed positon after peeking") assert.Equal(t, 3, len(z.buf), "must have two tokens after peeking") assert.Equal(t, svg.Svg, z.Peek(8).Hash, "nineth token must be <svg>") assert.Equal(t, 0, z.pos, "must not have changed positon after peeking") assert.Equal(t, 9, len(z.buf), "must have nine tokens after peeking") assert.Equal(t, xml.ErrorToken, z.Peek(9).TokenType, "tenth token must be error") assert.Equal(t, z.Peek(9), z.Peek(10), "tenth and eleventh token must both be EOF") assert.Equal(t, 10, len(z.buf), "must have ten tokens after peeking") tok = z.Shift() tok = z.Shift() assert.Equal(t, svg.Path, tok.Hash, "third token must be <path>") assert.Equal(t, 2, z.pos, "must not have changed positon after peeking") }
func TestBuffer(t *testing.T) { // 0 12 3 45 6 7 8 9 0 s := `<p><a href="//url">text</a>text<!--comment--></p>` z := NewTokenBuffer(xml.NewLexer(bytes.NewBufferString(s))) tok := z.Shift() assert.Equal(t, "p", string(tok.Data), "first token must be <p>") assert.Equal(t, 0, z.pos, "must have shifted first token and restored position") assert.Equal(t, 0, len(z.buf), "must have shifted first token and restored length") assert.Equal(t, "href", string(z.Peek(2).Data), "third token must be href") assert.Equal(t, 0, z.pos, "must not have changed positon after peeking") assert.Equal(t, 3, len(z.buf), "must have two tokens after peeking") assert.Equal(t, "p", string(z.Peek(8).Data), "nineth token must be <p>") assert.Equal(t, 0, z.pos, "must not have changed positon after peeking") assert.Equal(t, 9, len(z.buf), "must have nine tokens after peeking") assert.Equal(t, xml.ErrorToken, z.Peek(9).TokenType, "tenth token must be error") assert.Equal(t, z.Peek(9), z.Peek(10), "tenth and eleventh token must both be EOF") assert.Equal(t, 10, len(z.buf), "must have ten tokens after peeking") tok = z.Shift() tok = z.Shift() assert.Equal(t, "a", string(tok.Data), "third token must be <a>") assert.Equal(t, 2, z.pos, "must not have changed positon after peeking") }
func TestBuffer(t *testing.T) { // 0 12 3 45 6 7 8 9 0 s := `<p><a href="//url">text</a>text<!--comment--></p>` z := NewTokenBuffer(xml.NewLexer(bytes.NewBufferString(s))) tok := z.Shift() test.That(t, string(tok.Text) == "p", "first token is <p>") test.That(t, z.pos == 0, "shift first token and restore position") test.That(t, len(z.buf) == 0, "shift first token and restore length") test.That(t, string(z.Peek(2).Text) == "href", "third token is href") test.That(t, z.pos == 0, "don't change positon after peeking") test.That(t, len(z.buf) == 3, "two tokens after peeking") test.That(t, string(z.Peek(8).Text) == "p", "nineth token is <p>") test.That(t, z.pos == 0, "don't change positon after peeking") test.That(t, len(z.buf) == 9, "nine tokens after peeking") test.That(t, z.Peek(9).TokenType == xml.ErrorToken, "tenth token is an error") test.That(t, z.Peek(9) == z.Peek(10), "tenth and eleventh token are EOF") test.That(t, len(z.buf) == 10, "ten tokens after peeking") tok = z.Shift() tok = z.Shift() test.That(t, string(tok.Text) == "a", "third token is <a>") test.That(t, z.pos == 2, "don't change positon after peeking") }
func BenchmarkAttributes(b *testing.B) { r := bytes.NewBufferString(`<rect x="0" y="1" width="2" height="3" rx="4" ry="5"/>`) l := xml.NewLexer(r) tb := NewTokenBuffer(l) tb.Shift() tb.Peek(6) for i := 0; i < b.N; i++ { tb.Attributes(svg.X, svg.Y, svg.Width, svg.Height, svg.Rx, svg.Ry) } }
func TestAttributes(t *testing.T) { r := bytes.NewBufferString(`<rect x="0" y="1" width="2" height="3" rx="4" ry="5"/>`) l := xml.NewLexer(r) tb := NewTokenBuffer(l) tb.Shift() for k := 0; k < 2; k++ { // run twice to ensure similar results attrs, _ := tb.Attributes(svg.X, svg.Y, svg.Width, svg.Height, svg.Rx, svg.Ry) for i := 0; i < 6; i++ { test.That(t, attrs[i] != nil, "attr must not be nil") val := string(attrs[i].AttrVal) j, _ := strconv.ParseInt(val, 10, 32) test.That(t, int(j) == i, "attr data is bad at position", i) } } }
// 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 XML 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 { precededBySpace := true // on true the next text token must not start with a space attrByteBuffer := make([]byte, 0, 64) l := xml.NewLexer(r) tb := NewTokenBuffer(l) for { t := *tb.Shift() if t.TokenType == xml.CDATAToken { var useText bool if t.Data, useText = xml.EscapeCDATAVal(&attrByteBuffer, t.Data); useText { t.TokenType = xml.TextToken } } switch t.TokenType { case xml.ErrorToken: if l.Err() == io.EOF { return nil } return l.Err() case xml.DOCTYPEToken: if _, err := w.Write(t.Data); err != nil { return err } case xml.CDATAToken: if _, err := w.Write(t.Data); err != nil { return err } case xml.TextToken: if t.Data = parse.ReplaceMultipleWhitespace(t.Data); len(t.Data) > 0 { // whitespace removal; trim left if precededBySpace && t.Data[0] == ' ' { t.Data = t.Data[1:] } // whitespace removal; trim right precededBySpace = false if len(t.Data) == 0 { precededBySpace = true } else if t.Data[len(t.Data)-1] == ' ' { precededBySpace = true i := 0 for { next := tb.Peek(i) // trim if EOF, text token with whitespace begin or block token if next.TokenType == xml.StartTagToken || next.TokenType == xml.EndTagToken || next.TokenType == xml.ErrorToken { t.Data = t.Data[:len(t.Data)-1] precededBySpace = false break } else if next.TokenType == xml.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] precededBySpace = false } break } i++ } } if _, err := w.Write(t.Data); err != nil { return err } } case xml.StartTagToken: if _, err := w.Write(t.Data); err != nil { return err } case xml.StartTagPIToken: if _, err := w.Write(t.Data); err != nil { return err } case xml.AttributeToken: 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 len(t.AttrVal) < 2 { if _, err := w.Write(t.AttrVal); err != nil { return err } } else { // prefer single or double quotes depending on what occurs more often in value val := xml.EscapeAttrVal(&attrByteBuffer, t.AttrVal[1:len(t.AttrVal)-1]) 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.Text); err != nil { return err } } case xml.StartTagCloseVoidToken: if _, err := w.Write(t.Text); err != nil { return err } case xml.StartTagClosePIToken: if _, err := w.Write(t.Text); err != nil { return err } case xml.EndTagToken: t.Data[2+len(t.Text)] = '>' if _, err := w.Write(t.Data[:2+len(t.Text)+1]); 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 } } } }