xml.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. // Package xml minifies XML1.0 following the specifications at http://www.w3.org/TR/xml/.
  2. package xml // import "github.com/tdewolff/minify/xml"
  3. import (
  4. "io"
  5. "github.com/tdewolff/minify"
  6. "github.com/tdewolff/parse"
  7. "github.com/tdewolff/parse/xml"
  8. )
  9. var (
  10. isBytes = []byte("=")
  11. spaceBytes = []byte(" ")
  12. voidBytes = []byte("/>")
  13. )
  14. ////////////////////////////////////////////////////////////////
  15. // DefaultMinifier is the default minifier.
  16. var DefaultMinifier = &Minifier{}
  17. // Minifier is an XML minifier.
  18. type Minifier struct {
  19. KeepWhitespace bool
  20. }
  21. // Minify minifies XML data, it reads from r and writes to w.
  22. func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error {
  23. return DefaultMinifier.Minify(m, w, r, params)
  24. }
  25. // Minify minifies XML data, it reads from r and writes to w.
  26. func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]string) error {
  27. omitSpace := true // on true the next text token must not start with a space
  28. attrByteBuffer := make([]byte, 0, 64)
  29. l := xml.NewLexer(r)
  30. defer l.Restore()
  31. tb := NewTokenBuffer(l)
  32. for {
  33. t := *tb.Shift()
  34. if t.TokenType == xml.CDATAToken {
  35. if text, useText := xml.EscapeCDATAVal(&attrByteBuffer, t.Text); useText {
  36. t.TokenType = xml.TextToken
  37. t.Data = text
  38. }
  39. }
  40. switch t.TokenType {
  41. case xml.ErrorToken:
  42. if l.Err() == io.EOF {
  43. return nil
  44. }
  45. return l.Err()
  46. case xml.DOCTYPEToken:
  47. if _, err := w.Write(t.Data); err != nil {
  48. return err
  49. }
  50. case xml.CDATAToken:
  51. if _, err := w.Write(t.Data); err != nil {
  52. return err
  53. }
  54. if len(t.Text) > 0 && parse.IsWhitespace(t.Text[len(t.Text)-1]) {
  55. omitSpace = true
  56. }
  57. case xml.TextToken:
  58. t.Data = parse.ReplaceMultipleWhitespace(t.Data)
  59. // whitespace removal; trim left
  60. if omitSpace && (t.Data[0] == ' ' || t.Data[0] == '\n') {
  61. t.Data = t.Data[1:]
  62. }
  63. // whitespace removal; trim right
  64. omitSpace = false
  65. if len(t.Data) == 0 {
  66. omitSpace = true
  67. } else if t.Data[len(t.Data)-1] == ' ' || t.Data[len(t.Data)-1] == '\n' {
  68. omitSpace = true
  69. i := 0
  70. for {
  71. next := tb.Peek(i)
  72. // trim if EOF, text token with whitespace begin or block token
  73. if next.TokenType == xml.ErrorToken {
  74. t.Data = t.Data[:len(t.Data)-1]
  75. omitSpace = false
  76. break
  77. } else if next.TokenType == xml.TextToken {
  78. // this only happens when a comment, doctype, cdata startpi tag was in between
  79. // remove if the text token starts with a whitespace
  80. if len(next.Data) > 0 && parse.IsWhitespace(next.Data[0]) {
  81. t.Data = t.Data[:len(t.Data)-1]
  82. omitSpace = false
  83. }
  84. break
  85. } else if next.TokenType == xml.CDATAToken {
  86. if len(next.Text) > 0 && parse.IsWhitespace(next.Text[0]) {
  87. t.Data = t.Data[:len(t.Data)-1]
  88. omitSpace = false
  89. }
  90. break
  91. } else if next.TokenType == xml.StartTagToken || next.TokenType == xml.EndTagToken {
  92. if !o.KeepWhitespace {
  93. t.Data = t.Data[:len(t.Data)-1]
  94. omitSpace = false
  95. }
  96. break
  97. }
  98. i++
  99. }
  100. }
  101. if _, err := w.Write(t.Data); err != nil {
  102. return err
  103. }
  104. case xml.StartTagToken:
  105. if o.KeepWhitespace {
  106. omitSpace = false
  107. }
  108. if _, err := w.Write(t.Data); err != nil {
  109. return err
  110. }
  111. case xml.StartTagPIToken:
  112. if _, err := w.Write(t.Data); err != nil {
  113. return err
  114. }
  115. case xml.AttributeToken:
  116. if _, err := w.Write(spaceBytes); err != nil {
  117. return err
  118. }
  119. if _, err := w.Write(t.Text); err != nil {
  120. return err
  121. }
  122. if _, err := w.Write(isBytes); err != nil {
  123. return err
  124. }
  125. if len(t.AttrVal) < 2 {
  126. if _, err := w.Write(t.AttrVal); err != nil {
  127. return err
  128. }
  129. } else {
  130. // prefer single or double quotes depending on what occurs more often in value
  131. val := xml.EscapeAttrVal(&attrByteBuffer, t.AttrVal[1:len(t.AttrVal)-1])
  132. if _, err := w.Write(val); err != nil {
  133. return err
  134. }
  135. }
  136. case xml.StartTagCloseToken:
  137. next := tb.Peek(0)
  138. skipExtra := false
  139. if next.TokenType == xml.TextToken && parse.IsAllWhitespace(next.Data) {
  140. next = tb.Peek(1)
  141. skipExtra = true
  142. }
  143. if next.TokenType == xml.EndTagToken {
  144. // collapse empty tags to single void tag
  145. tb.Shift()
  146. if skipExtra {
  147. tb.Shift()
  148. }
  149. if _, err := w.Write(voidBytes); err != nil {
  150. return err
  151. }
  152. } else {
  153. if _, err := w.Write(t.Text); err != nil {
  154. return err
  155. }
  156. }
  157. case xml.StartTagCloseVoidToken:
  158. if _, err := w.Write(t.Text); err != nil {
  159. return err
  160. }
  161. case xml.StartTagClosePIToken:
  162. if _, err := w.Write(t.Text); err != nil {
  163. return err
  164. }
  165. case xml.EndTagToken:
  166. if o.KeepWhitespace {
  167. omitSpace = false
  168. }
  169. if len(t.Data) > 3+len(t.Text) {
  170. t.Data[2+len(t.Text)] = '>'
  171. t.Data = t.Data[:3+len(t.Text)]
  172. }
  173. if _, err := w.Write(t.Data); err != nil {
  174. return err
  175. }
  176. }
  177. }
  178. }