gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }