minify.go (12238B)
1 // Package minify relates MIME type to minifiers. Several minifiers are provided in the subpackages. 2 package minify 3 4 import ( 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "mime" 12 "net/http" 13 "net/url" 14 "os" 15 "os/exec" 16 "path" 17 "regexp" 18 "strings" 19 "sync" 20 21 "github.com/tdewolff/parse/v2" 22 "github.com/tdewolff/parse/v2/buffer" 23 ) 24 25 // Warning is used to report usage warnings such as using a deprecated feature 26 var Warning = log.New(os.Stderr, "WARNING: ", 0) 27 28 // ErrNotExist is returned when no minifier exists for a given mimetype. 29 var ErrNotExist = errors.New("minifier does not exist for mimetype") 30 31 // ErrClosedWriter is returned when writing to a closed writer. 32 var ErrClosedWriter = errors.New("write on closed writer") 33 34 //////////////////////////////////////////////////////////////// 35 36 // MinifierFunc is a function that implements Minifer. 37 type MinifierFunc func(*M, io.Writer, io.Reader, map[string]string) error 38 39 // Minify calls f(m, w, r, params) 40 func (f MinifierFunc) Minify(m *M, w io.Writer, r io.Reader, params map[string]string) error { 41 return f(m, w, r, params) 42 } 43 44 // Minifier is the interface for minifiers. 45 // The *M parameter is used for minifying embedded resources, such as JS within HTML. 46 type Minifier interface { 47 Minify(*M, io.Writer, io.Reader, map[string]string) error 48 } 49 50 //////////////////////////////////////////////////////////////// 51 52 type patternMinifier struct { 53 pattern *regexp.Regexp 54 Minifier 55 } 56 57 type cmdMinifier struct { 58 cmd *exec.Cmd 59 } 60 61 var cmdArgExtension = regexp.MustCompile(`^\.[0-9a-zA-Z]+`) 62 63 func (c *cmdMinifier) Minify(_ *M, w io.Writer, r io.Reader, _ map[string]string) error { 64 cmd := &exec.Cmd{} 65 *cmd = *c.cmd // concurrency safety 66 67 var in, out *os.File 68 for i, arg := range cmd.Args { 69 if j := strings.Index(arg, "$in"); j != -1 { 70 var err error 71 ext := cmdArgExtension.FindString(arg[j+3:]) 72 if in, err = ioutil.TempFile("", "minify-in-*"+ext); err != nil { 73 return err 74 } 75 cmd.Args[i] = arg[:j] + in.Name() + arg[j+3+len(ext):] 76 } else if j := strings.Index(arg, "$out"); j != -1 { 77 var err error 78 ext := cmdArgExtension.FindString(arg[j+4:]) 79 if out, err = ioutil.TempFile("", "minify-out-*"+ext); err != nil { 80 return err 81 } 82 cmd.Args[i] = arg[:j] + out.Name() + arg[j+4+len(ext):] 83 } 84 } 85 86 if in == nil { 87 cmd.Stdin = r 88 } else if _, err := io.Copy(in, r); err != nil { 89 return err 90 } 91 if out == nil { 92 cmd.Stdout = w 93 } else { 94 defer io.Copy(w, out) 95 } 96 stderr := &bytes.Buffer{} 97 cmd.Stderr = stderr 98 99 err := cmd.Run() 100 if _, ok := err.(*exec.ExitError); ok { 101 if stderr.Len() != 0 { 102 err = fmt.Errorf("%s", stderr.String()) 103 } 104 err = fmt.Errorf("command %s failed: %w", cmd.Path, err) 105 } 106 return err 107 } 108 109 //////////////////////////////////////////////////////////////// 110 111 // M holds a map of mimetype => function to allow recursive minifier calls of the minifier functions. 112 type M struct { 113 mutex sync.RWMutex 114 literal map[string]Minifier 115 pattern []patternMinifier 116 117 URL *url.URL 118 } 119 120 // New returns a new M. 121 func New() *M { 122 return &M{ 123 sync.RWMutex{}, 124 map[string]Minifier{}, 125 []patternMinifier{}, 126 nil, 127 } 128 } 129 130 // Add adds a minifier to the mimetype => function map (unsafe for concurrent use). 131 func (m *M) Add(mimetype string, minifier Minifier) { 132 m.mutex.Lock() 133 m.literal[mimetype] = minifier 134 m.mutex.Unlock() 135 } 136 137 // AddFunc adds a minify function to the mimetype => function map (unsafe for concurrent use). 138 func (m *M) AddFunc(mimetype string, minifier MinifierFunc) { 139 m.mutex.Lock() 140 m.literal[mimetype] = minifier 141 m.mutex.Unlock() 142 } 143 144 // AddRegexp adds a minifier to the mimetype => function map (unsafe for concurrent use). 145 func (m *M) AddRegexp(pattern *regexp.Regexp, minifier Minifier) { 146 m.mutex.Lock() 147 m.pattern = append(m.pattern, patternMinifier{pattern, minifier}) 148 m.mutex.Unlock() 149 } 150 151 // AddFuncRegexp adds a minify function to the mimetype => function map (unsafe for concurrent use). 152 func (m *M) AddFuncRegexp(pattern *regexp.Regexp, minifier MinifierFunc) { 153 m.mutex.Lock() 154 m.pattern = append(m.pattern, patternMinifier{pattern, minifier}) 155 m.mutex.Unlock() 156 } 157 158 // AddCmd adds a minify function to the mimetype => function map (unsafe for concurrent use) that executes a command to process the minification. 159 // It allows the use of external tools like ClosureCompiler, UglifyCSS, etc. for a specific mimetype. 160 func (m *M) AddCmd(mimetype string, cmd *exec.Cmd) { 161 m.mutex.Lock() 162 m.literal[mimetype] = &cmdMinifier{cmd} 163 m.mutex.Unlock() 164 } 165 166 // AddCmdRegexp adds a minify function to the mimetype => function map (unsafe for concurrent use) that executes a command to process the minification. 167 // It allows the use of external tools like ClosureCompiler, UglifyCSS, etc. for a specific mimetype regular expression. 168 func (m *M) AddCmdRegexp(pattern *regexp.Regexp, cmd *exec.Cmd) { 169 m.mutex.Lock() 170 m.pattern = append(m.pattern, patternMinifier{pattern, &cmdMinifier{cmd}}) 171 m.mutex.Unlock() 172 } 173 174 // Match returns the pattern and minifier that gets matched with the mediatype. 175 // It returns nil when no matching minifier exists. 176 // It has the same matching algorithm as Minify. 177 func (m *M) Match(mediatype string) (string, map[string]string, MinifierFunc) { 178 m.mutex.RLock() 179 defer m.mutex.RUnlock() 180 181 mimetype, params := parse.Mediatype([]byte(mediatype)) 182 if minifier, ok := m.literal[string(mimetype)]; ok { // string conversion is optimized away 183 return string(mimetype), params, minifier.Minify 184 } 185 186 for _, minifier := range m.pattern { 187 if minifier.pattern.Match(mimetype) { 188 return minifier.pattern.String(), params, minifier.Minify 189 } 190 } 191 return string(mimetype), params, nil 192 } 193 194 // Minify minifies the content of a Reader and writes it to a Writer (safe for concurrent use). 195 // An error is returned when no such mimetype exists (ErrNotExist) or when an error occurred in the minifier function. 196 // Mediatype may take the form of 'text/plain', 'text/*', '*/*' or 'text/plain; charset=UTF-8; version=2.0'. 197 func (m *M) Minify(mediatype string, w io.Writer, r io.Reader) error { 198 mimetype, params := parse.Mediatype([]byte(mediatype)) 199 return m.MinifyMimetype(mimetype, w, r, params) 200 } 201 202 // MinifyMimetype minifies the content of a Reader and writes it to a Writer (safe for concurrent use). 203 // It is a lower level version of Minify and requires the mediatype to be split up into mimetype and parameters. 204 // It is mostly used internally by minifiers because it is faster (no need to convert a byte-slice to string and vice versa). 205 func (m *M) MinifyMimetype(mimetype []byte, w io.Writer, r io.Reader, params map[string]string) error { 206 m.mutex.RLock() 207 defer m.mutex.RUnlock() 208 209 if minifier, ok := m.literal[string(mimetype)]; ok { // string conversion is optimized away 210 return minifier.Minify(m, w, r, params) 211 } 212 for _, minifier := range m.pattern { 213 if minifier.pattern.Match(mimetype) { 214 return minifier.Minify(m, w, r, params) 215 } 216 } 217 return ErrNotExist 218 } 219 220 // Bytes minifies an array of bytes (safe for concurrent use). When an error occurs it return the original array and the error. 221 // It returns an error when no such mimetype exists (ErrNotExist) or any error occurred in the minifier function. 222 func (m *M) Bytes(mediatype string, v []byte) ([]byte, error) { 223 out := buffer.NewWriter(make([]byte, 0, len(v))) 224 if err := m.Minify(mediatype, out, buffer.NewReader(v)); err != nil { 225 return v, err 226 } 227 return out.Bytes(), nil 228 } 229 230 // String minifies a string (safe for concurrent use). When an error occurs it return the original string and the error. 231 // It returns an error when no such mimetype exists (ErrNotExist) or any error occurred in the minifier function. 232 func (m *M) String(mediatype string, v string) (string, error) { 233 out := buffer.NewWriter(make([]byte, 0, len(v))) 234 if err := m.Minify(mediatype, out, buffer.NewReader([]byte(v))); err != nil { 235 return v, err 236 } 237 return string(out.Bytes()), nil 238 } 239 240 // Reader wraps a Reader interface and minifies the stream. 241 // Errors from the minifier are returned by the reader. 242 func (m *M) Reader(mediatype string, r io.Reader) io.Reader { 243 pr, pw := io.Pipe() 244 go func() { 245 if err := m.Minify(mediatype, pw, r); err != nil { 246 pw.CloseWithError(err) 247 } else { 248 pw.Close() 249 } 250 }() 251 return pr 252 } 253 254 // writer makes sure that errors from the minifier are passed down through Close (can be blocking). 255 type writer struct { 256 pw *io.PipeWriter 257 wg sync.WaitGroup 258 err error 259 closed bool 260 } 261 262 // Write intercepts any writes to the writer. 263 func (w *writer) Write(b []byte) (int, error) { 264 if w.closed { 265 return 0, ErrClosedWriter 266 } 267 n, err := w.pw.Write(b) 268 if w.err != nil { 269 err = w.err 270 } 271 return n, err 272 } 273 274 // Close must be called when writing has finished. It returns the error from the minifier. 275 func (w *writer) Close() error { 276 if !w.closed { 277 w.pw.Close() 278 w.wg.Wait() 279 w.closed = true 280 } 281 return w.err 282 } 283 284 // Writer wraps a Writer interface and minifies the stream. 285 // Errors from the minifier are returned by Close on the writer. 286 // The writer must be closed explicitly. 287 func (m *M) Writer(mediatype string, w io.Writer) io.WriteCloser { 288 pr, pw := io.Pipe() 289 mw := &writer{pw, sync.WaitGroup{}, nil, false} 290 mw.wg.Add(1) 291 go func() { 292 defer mw.wg.Done() 293 294 if err := m.Minify(mediatype, w, pr); err != nil { 295 mw.err = err 296 } 297 pr.Close() 298 }() 299 return mw 300 } 301 302 // responseWriter wraps an http.ResponseWriter and makes sure that errors from the minifier are passed down through Close (can be blocking). 303 // All writes to the response writer are intercepted and minified on the fly. 304 // http.ResponseWriter loses all functionality such as Pusher, Hijacker, Flusher, ... 305 type responseWriter struct { 306 http.ResponseWriter 307 308 writer *writer 309 m *M 310 mediatype string 311 } 312 313 // WriteHeader intercepts any header writes and removes the Content-Length header. 314 func (w *responseWriter) WriteHeader(status int) { 315 w.ResponseWriter.Header().Del("Content-Length") 316 w.ResponseWriter.WriteHeader(status) 317 } 318 319 // Write intercepts any writes to the response writer. 320 // The first write will extract the Content-Type as the mediatype. Otherwise it falls back to the RequestURI extension. 321 func (w *responseWriter) Write(b []byte) (int, error) { 322 if w.writer == nil { 323 // first write 324 if mediatype := w.ResponseWriter.Header().Get("Content-Type"); mediatype != "" { 325 w.mediatype = mediatype 326 } 327 w.writer = w.m.Writer(w.mediatype, w.ResponseWriter).(*writer) 328 } 329 return w.writer.Write(b) 330 } 331 332 // Close must be called when writing has finished. It returns the error from the minifier. 333 func (w *responseWriter) Close() error { 334 if w.writer != nil { 335 return w.writer.Close() 336 } 337 return nil 338 } 339 340 // ResponseWriter minifies any writes to the http.ResponseWriter. 341 // http.ResponseWriter loses all functionality such as Pusher, Hijacker, Flusher, ... 342 // Minification might be slower than just sending the original file! Caching is advised. 343 func (m *M) ResponseWriter(w http.ResponseWriter, r *http.Request) *responseWriter { 344 mediatype := mime.TypeByExtension(path.Ext(r.RequestURI)) 345 return &responseWriter{w, nil, m, mediatype} 346 } 347 348 // Middleware provides a middleware function that minifies content on the fly by intercepting writes to http.ResponseWriter. 349 // http.ResponseWriter loses all functionality such as Pusher, Hijacker, Flusher, ... 350 // Minification might be slower than just sending the original file! Caching is advised. 351 func (m *M) Middleware(next http.Handler) http.Handler { 352 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 353 mw := m.ResponseWriter(w, r) 354 next.ServeHTTP(mw, r) 355 mw.Close() 356 }) 357 } 358 359 // MiddlewareWithError provides a middleware function that minifies content on the fly by intercepting writes to http.ResponseWriter. The error function allows handling minification errors. 360 // http.ResponseWriter loses all functionality such as Pusher, Hijacker, Flusher, ... 361 // Minification might be slower than just sending the original file! Caching is advised. 362 func (m *M) MiddlewareWithError(next http.Handler, errorFunc func(w http.ResponseWriter, r *http.Request, err error)) http.Handler { 363 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 364 mw := m.ResponseWriter(w, r) 365 next.ServeHTTP(mw, r) 366 if err := mw.Close(); err != nil { 367 errorFunc(w, r, err) 368 return 369 } 370 }) 371 }