diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index c09af7ec..ecbcb577 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -68,3 +68,9 @@ updates:
- "🤖 Dependencies"
schedule:
interval: "daily"
+ - package-ecosystem: "gomod"
+ directory: "/minify/" # Location of package manifests
+ labels:
+ - "🤖 Dependencies"
+ schedule:
+ interval: "daily"
diff --git a/.github/testdata/.editorconfig b/.github/testdata/.editorconfig
new file mode 100644
index 00000000..17542073
--- /dev/null
+++ b/.github/testdata/.editorconfig
@@ -0,0 +1,5 @@
+; https://editorconfig.org/
+root = true
+
+[.github/testdata2/*]
+end_of_line = unset
\ No newline at end of file
diff --git a/.github/testdata/minify_expected_data.yaml b/.github/testdata/minify_expected_data.yaml
new file mode 100644
index 00000000..1293d258
--- /dev/null
+++ b/.github/testdata/minify_expected_data.yaml
@@ -0,0 +1,77 @@
+# test cases expected data
+HtmlMinify:
DocumentHello, Fiber!
+html_with_css_default: >-
+ DocumentHello, Fiber!
+html_with_js_default: >-
+ DocumentHello, Fiber!
+html_with_css_and_js: >-
+ DocumentHello, Fiber!
+html_config_options_styles_false: >-
+ DocumentHello, Fiber!
+html_config_options_js_false: >-
+ DocumentHello, Fiber!
+html_config_options_css_js_false: >-
+ DocumentHello, Fiber!
+html_config_options_css_js_true: DocumentHello, Fiber!
+css_minify: body{font-family:Arial,sans-serif;font-size:16px;line-height:1.5;color:#333;background-color:#fff}h1{font-size:32px;margin-bottom:10px}@media only screen and (max-width:768px){body{font-size:14px;line-height:1.3}h1{font-size:24px;margin-bottom:5px}}@media only screen and (max-width:480px){body{font-size:12px;line-height:1.2}h1{font-size:20px;margin-bottom:5px}}
+js_minify: var x=10;function factorial(n){if(n<=1){return 1;};return n*factorial(n-1);};var double=function(x){return x*2;};var numbers=[1,2,3,4,5];for(var i=0;i
+
+
+
+
+
+ Document
+
+
+
+
+ Hello, Fiber!
+
+
+
\ No newline at end of file
diff --git a/.github/testdata/template-with-css.html b/.github/testdata/template-with-css.html
new file mode 100644
index 00000000..e188b172
--- /dev/null
+++ b/.github/testdata/template-with-css.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Document
+
+
+
+ Hello, Fiber!
+
+
\ No newline at end of file
diff --git a/.github/testdata/template-with-js.html b/.github/testdata/template-with-js.html
new file mode 100644
index 00000000..74b1c54b
--- /dev/null
+++ b/.github/testdata/template-with-js.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Document
+
+
+
+ Hello, Fiber!
+
+
\ No newline at end of file
diff --git a/.github/testdata/template.css b/.github/testdata/template.css
new file mode 100644
index 00000000..5abaf34a
--- /dev/null
+++ b/.github/testdata/template.css
@@ -0,0 +1,40 @@
+/* This is a comment
+ spanning multiple lines */
+body {
+ font-family: Arial, sans-serif;
+ font-size: 16px;
+ line-height: 1.5;
+ color: #333;
+ background-color: #fff;
+ }
+
+ /* This is a single-line comment */
+ h1 {
+ font-size: 32px;
+ margin-bottom: 10px;
+ }
+
+ @media only screen and (max-width: 768px) {
+ /* This is a nested comment */
+ body {
+ font-size: 14px;
+ line-height: 1.3;
+ }
+
+ h1 {
+ font-size: 24px;
+ margin-bottom: 5px;
+ }
+ }
+
+ @media only screen and (max-width: 480px) {
+ body {
+ font-size: 12px;
+ line-height: 1.2;
+ }
+
+ h1 {
+ font-size: 20px;
+ margin-bottom: 5px;
+ }
+ }
\ No newline at end of file
diff --git a/.github/testdata/template.html b/.github/testdata/template.html
new file mode 100644
index 00000000..16faa993
--- /dev/null
+++ b/.github/testdata/template.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ Document
+
+
+ Hello, Fiber!
+
+
+
+
+
\ No newline at end of file
diff --git a/.github/testdata/template.js b/.github/testdata/template.js
new file mode 100644
index 00000000..df56985f
--- /dev/null
+++ b/.github/testdata/template.js
@@ -0,0 +1,40 @@
+// This is a single-line comment
+var x = 10; // This is also a single-line comment
+
+/*
+This is a multi-line comment
+It can span across multiple lines
+*/
+
+// Function that returns the factorial of a number
+function factorial(n) {
+ if (n <= 1) {
+ return 1;
+ };
+ return n * factorial(n - 1); // Recursive call
+};
+
+// Anonymous function assigned to a variable
+var double = function(x) {
+ return x * 2;
+};
+
+// Array of numbers
+var numbers = [1, 2, 3, 4, 5];
+
+// Loop through the array and double each number
+for (var i = 0; i < numbers.length; i++) {
+ numbers[i] = double(numbers[i]);
+};
+
+// Object with properties and methods
+var person = {
+ name: "John",
+ age: 30,
+ greet: function() {
+ console.log("Hello, my name is " + this.name + " and I'm " + this.age + " years old.");
+ }
+};
+
+// Call the greet method
+person.greet();
diff --git a/.github/workflows/release-drafter-minify.yml b/.github/workflows/release-drafter-minify.yml
new file mode 100644
index 00000000..b70374a4
--- /dev/null
+++ b/.github/workflows/release-drafter-minify.yml
@@ -0,0 +1,19 @@
+name: Release Drafter Minify
+on:
+ push:
+ # branches to consider in the event; optional, defaults to all
+ branches:
+ - master
+ - main
+ paths:
+ - 'minify/**'
+jobs:
+ draft_release_casbin:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: release-drafter/release-drafter@v5
+ with:
+ config-name: release-drafter-minify.yml
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index 80d6dc2f..d8577ef6 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -56,3 +56,8 @@ jobs:
working-directory: ./fiberzerolog
run: "`go env GOPATH`/bin/gosec -exclude-dir=internal ./..."
# -----
+ # -----
+ - name: Run Gosec (minify)
+ working-directory: ./minify
+ run: "`go env GOPATH`/bin/gosec -exclude-dir=internal ./..."
+ # -----
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 992d2ff9..1e9d2d54 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -58,3 +58,5 @@ jobs:
run: cd ./fiberi18n && go test ./... -v -race
- name: Test fiberzerolog Middleware
run: cd ./fiberzerolog && go test ./... -v -race
+ - name: Test minify Middleware
+ run: cd ./minify && go test ./... -v race
diff --git a/minify/README.md b/minify/README.md
new file mode 100644
index 00000000..282af70f
--- /dev/null
+++ b/minify/README.md
@@ -0,0 +1,80 @@
+# Minify
+Minify middleware for [Fiber](https://github.com/gofiber/fiber). The middleware handles minifying HTML, CSS and JavaScript responses.
+
+### Table of Contents
+- [Signatures](#signatures)
+- [Examples](#examples)
+- [Config](#config)
+- [Minify HTML Options](#minifyhtmloptions)
+- [Minify CSS Options](#minifycssoptions)
+- [Minify JS Options](#minifyjsoptions)
+
+
+### Signatures
+```go
+func New(config ...Config) fiber.Handler
+```
+
+### Examples
+Import the middleware package that is part of the Fiber web framework
+```go
+import (
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/contrib/minify"
+)
+```
+
+Then create a Fiber app with app := fiber.New().
+
+After you initiate your Fiber app, you can use the following possibilities:
+
+### Default Config
+
+```go
+app.Use(minify.New())
+```
+
+### Custom Config
+
+```go
+cfg := minify.Config{
+ MinifyHTML: true,
+ MinifyHTMLOptions: MinifyHTMLOptions{
+ MinifyScripts: true,
+ MinifyStyles: true
+ },
+ MinifyCSS: true,
+ MinifyJS: true,
+ Method: GET,
+}
+
+app.Use(minify.New(cfg))
+```
+### Config
+| Property | Type | Description | Optional | Default |
+| :--- | :--- | :--- | :--- | :--- |
+| MinifyHTML | `bool` | Enable / Disable Html minfy | `yes` | `true` |
+| MinifyHTMLOptions | `MinifyHTMLOptions` | [Options for the MinifyHTML](#minifyhTMLoptions) | `yes` | `MinifyHTMLOptions` |
+| MinifyCSS | `bool` | Enable / Disable CSS minfy | `yes` | `false` |
+| MinifyCSSOptions | `MinifyCSSOptions` | [Options for the MinifyCSS](#MinifyCSSOptions) | `yes` | `MinifyCSSOptions` |
+| MinifyJS | `bool` | Enable / Disable JS minfy | `yes` | `false` |
+| MinifyJSOptions | `MinifyJSOptions` | [Options for the MinifyJS](#MinifyJSOptions) | `yes` | `MinifyJSOptions` |
+| Method | `Method` | Representation of minify route method | `yes` | `GET` |
+| Next | `func(c *fiber.Ctx) bool` | Skip this middleware when returned true | `yes` | `nil` |
+
+### MinifyHTMLOptions
+| Property | Type | Description | Default |
+| :--- | :--- | :--- | :--- |
+| MinifyScripts | `bool` | Whether scripts inside the HTML should be minified or not | `false` |
+| MinifyStyles | `bool` | Whether styles inside the HTML should be minified or not | `false` |
+| ExcludeURLs | `[]string` | URLs Exclud from minification | `nil` |
+
+### MinifyCSSOptions
+| Property | Type | Description | Default |
+| :--- | :--- | :--- | :--- |
+| ExcludeStyles | `[]string` | Styles exclud from minification | `[]string{"*.min.css", "*.bundle.css"}` |
+
+### MinifyJSOptions
+| Property | Type | Description | Default |
+| :--- | :--- | :--- | :--- |
+| ExcludeScripts | `[]string` | Styles exclud from minification | `[]string{"*.min.js", "*.bundle.js"}` |
\ No newline at end of file
diff --git a/minify/config.go b/minify/config.go
new file mode 100644
index 00000000..fc14ae3a
--- /dev/null
+++ b/minify/config.go
@@ -0,0 +1,150 @@
+package minify
+
+import (
+ "github.com/gofiber/fiber/v2"
+)
+
+// Config defines the config for middleware.
+type Config struct {
+ // Next defines a function to skip this middleware when returned true.
+ // Optional. Default: nil
+ Next func(c *fiber.Ctx) bool
+
+ // MinifyHTML is a boolean value that indicates whether HTML responses should be minified.
+ // Optional. Default: true
+ MinifyHTML bool
+
+ // MinifyHTMLOptions is used to configure the HTML minifier.
+ // Optional.
+ MinifyHTMLOptions MinifyHTMLOptions
+
+ // MinifyCSS is a boolean value that indicates whether CSS responses should be minified.
+ // Optional. Default: false
+ MinifyCSS bool
+
+ // MinifyCSSOptions is used to configure the CSS minifier.
+ // Optional.
+ MinifyCSSOptions MinifyCSSOptions
+
+ // MinifyJS is a boolean value that indicates whether JavaScript responses should be minified.
+ // Optional. Default: false
+ MinifyJS bool
+
+ // MinifyJSOptions is used to configure the JavaScript minifier.
+ // Optional.
+ MinifyJSOptions MinifyJSOptions
+
+ // Method is a string representation of minify route method.
+ // Possible values: GET, HEAD, POST, ALL.
+ // Optional. Default: GET (only minify GET requests).
+ Method Method
+}
+
+type MinifyHTMLOptions struct {
+ // boolean value that indicates whether scripts inside the HTML should be minified or not.
+ // Optional. Default: false
+ MinifyScripts bool
+
+ // boolean value that indicates whether styles inside the HTML should be minified or not.
+ // Optional. Default: false
+ MinifyStyles bool
+
+ // ExcludeURLs is a slice of strings that contains URLs that should be excluded from minification.
+ // Possible patterns: "/exact-url", "urlgroup/*"
+ // Optional. Default: nil
+ ExcludeURLs []string
+}
+
+type MinifyCSSOptions struct {
+ // ExcludeURLs is a slice of strings that contains URLs to the styles that should be excluded from minification.
+ // Possible patterns: "/path/to/style.css", "/path/to/*", "*.min.css"
+ // Optional. Default: "*.min.css", "*.bundle.css"
+ ExcludeStyles []string
+}
+
+type MinifyJSOptions struct {
+ // ExcludeURLs is a slice of strings that contains URLs to the scripts that should be excluded from minification.
+ // Possible patterns: "/path/to/script.js", "/path/to/*", "*.min.js"
+ // Optional. Default: "*.min.js", "*.bundle.js"
+ ExcludeScripts []string
+}
+
+// Method is a string representation of minify route method
+type Method string
+
+// Represents minify method that will be used in the middleware
+const (
+ GET Method = "GET"
+ HEAD Method = "HEAD"
+ POST Method = "POST"
+ ALL Method = "ALL"
+)
+
+// ConfigDefault is the default config
+var ConfigDefault = Config{
+ Next: nil,
+ MinifyHTML: true,
+ MinifyHTMLOptions: MinifyHTMLOptions{
+ MinifyScripts: false,
+ MinifyStyles: false,
+ ExcludeURLs: nil,
+ },
+ MinifyCSS: false,
+ MinifyCSSOptions: MinifyCSSOptions{
+ ExcludeStyles: []string{"*.min.css", "*.bundle.css"},
+ },
+ MinifyJS: false,
+ MinifyJSOptions: MinifyJSOptions{
+ ExcludeScripts: []string{"*.min.js", "*.bundle.js"},
+ },
+ Method: GET,
+}
+
+// Helper function to set default values
+func configDefault(config ...Config) Config {
+ // Return default config if nothing provided
+ if len(config) < 1 {
+ return ConfigDefault
+ }
+
+ // Override default config
+ cfg := config[0]
+
+ // Set default values
+ if cfg.Method == "" {
+ cfg.Method = ConfigDefault.Method
+ }
+
+ if equalMinifyHTMLOptions(cfg.MinifyHTMLOptions, &MinifyHTMLOptions{}) {
+ cfg.MinifyHTMLOptions = ConfigDefault.MinifyHTMLOptions
+ }
+
+ if cfg.MinifyCSSOptions.ExcludeStyles == nil {
+ cfg.MinifyCSSOptions = ConfigDefault.MinifyCSSOptions
+ }
+
+ if cfg.MinifyJSOptions.ExcludeScripts == nil {
+ cfg.MinifyJSOptions = ConfigDefault.MinifyJSOptions
+ }
+ return cfg
+}
+
+// Helper function to compare MinifyHTMLOptions
+func equalMinifyHTMLOptions(a MinifyHTMLOptions, b *MinifyHTMLOptions) bool {
+ return a.MinifyScripts == b.MinifyScripts &&
+ a.MinifyStyles == b.MinifyStyles &&
+ equalStringSlices(a.ExcludeURLs, b.ExcludeURLs)
+}
+
+// Helper function to compare string slices
+func equalStringSlices(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i, v := range a {
+ if v != b[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/minify/css_minify.go b/minify/css_minify.go
new file mode 100644
index 00000000..82e49ba4
--- /dev/null
+++ b/minify/css_minify.go
@@ -0,0 +1,111 @@
+package minify
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+ "strconv"
+)
+
+// define regular expressions to match different patterns in CSS code
+var (
+ rcomments = regexp.MustCompile(`\/\*[\s\S]*?\*\/`)
+ rwhitespace = regexp.MustCompile(`\s+`)
+ runits = regexp.MustCompile(`(?i)([\s:])([+-]?0)(?:%|em|ex|px|in|cm|mm|pt|pc)`)
+ rfourzero = regexp.MustCompile(`:(?:0 )+0;`)
+ rleadzero = regexp.MustCompile(`(:|\s)0+\.(\d+)`)
+ rrgb = regexp.MustCompile(`rgb\s*\(\s*([0-9,\s]+)\s*\)`)
+ rbmh = regexp.MustCompile(`"\\"\}\\""`)
+ runspace1 = regexp.MustCompile(`(?:^|\})[^\{:]+\s+:+[^\{]*\{`)
+ runspace2 = regexp.MustCompile(`\s+([!\{\};:>+\(\)\],])`)
+ rcompresshex = regexp.MustCompile(`(?i)([^"'=\s])(\s?)\s*#([0-9a-f]){6}`)
+ rhexval = regexp.MustCompile(`[0-9a-f]{2}`)
+ remptyrules = regexp.MustCompile(`[^\}]+\{;\}\n`)
+ rmediaspace = regexp.MustCompile(`\band\(`)
+ rredsemicolons = regexp.MustCompile(`;+\}`)
+ runspace3 = regexp.MustCompile(`([!\{\}:;>+\(\[,])\s+`)
+ rsemicolons = regexp.MustCompile(`([^;\}])\}`)
+ rdigits = regexp.MustCompile(`\d+`)
+)
+
+// cssMinify takes in a slice of bytes representing CSS code and returns a minified version of it.
+func cssMinify(css []byte) (minified []byte) {
+ // Remove // and /* */ CSS comments
+ css = rcomments.ReplaceAll(css, []byte{})
+
+ // replace whitespace with a single space character
+ css = rwhitespace.ReplaceAll(css, []byte(" "))
+
+ // Replace all occurrences of '}' with '___BMH___' (Block Marker Hash)
+ css = rbmh.ReplaceAll(css, []byte("___BMH___"))
+
+ // Replace all occurrences of ':' with '___PSEUDOCLASSCOLON___'
+ css = runspace1.ReplaceAllFunc(css, func(match []byte) []byte {
+ return bytes.Replace(match, []byte(":"), []byte("___PSEUDOCLASSCOLON___"), -1)
+ })
+ css = runspace2.ReplaceAll(css, []byte("$1"))
+ css = bytes.Replace(css, []byte("___PSEUDOCLASSCOLON___"), []byte(":"), -1)
+
+ // remove space after commas, colons, semicolons, brackets, etc.
+ css = runspace3.ReplaceAll(css, []byte("$1"))
+
+ // add missing semicolon
+ css = rsemicolons.ReplaceAll(css, []byte("$1;}"))
+
+ // remove leading zeros from integer values
+ css = runits.ReplaceAll(css, []byte("$1$2"))
+
+ // replace 0 0 0 0; with 0;
+ css = rfourzero.ReplaceAll(css, []byte(":0;"))
+
+ // replace background-position:0; with background-position:0 0;
+ css = bytes.Replace(css, []byte("background-position:0;"), []byte("background-position:0 0;"), -1)
+
+ // remove leading zeros from float values
+ css = rleadzero.ReplaceAll(css, []byte("$1.$2"))
+
+ // replace rgb(0,0,0) with #000
+ css = rrgb.ReplaceAllFunc(css, func(match []byte) (out []byte) {
+ out = []byte{'#'}
+ for _, v := range rdigits.FindAll(match, -1) {
+ d, err := strconv.Atoi(string(v))
+ if err != nil {
+ return match
+ }
+ out = append(out, []byte(fmt.Sprintf("%02x", d))...)
+ }
+ return out
+ })
+ // replace #aabbcc with #abc
+ css = rcompresshex.ReplaceAllFunc(css, func(match []byte) (out []byte) {
+ vals := rhexval.FindAll(match, -1)
+ if len(vals) != 3 {
+ return match
+ }
+ compressible := true
+ for _, v := range vals {
+ if v[0] != v[1] {
+ compressible = false
+ }
+ }
+ if !compressible {
+ return match
+ }
+ out = append(out, match[:bytes.IndexByte(match, '#')+1]...)
+ return append(out, vals[0][0], vals[1][0], vals[2][0])
+ })
+
+ // remove empty rules
+ css = remptyrules.ReplaceAll(css, []byte{})
+
+ // replace ___BMH___ with '}' (put back the removed closing brackets)
+ css = bytes.Replace(css, []byte("___BMH___"), []byte(`"\"}\""`), -1)
+
+ // replace 'and (' with 'and('
+ css = rmediaspace.ReplaceAll(css, []byte("and ("))
+
+ // remove trailing semicolons
+ css = rredsemicolons.ReplaceAll(css, []byte("}"))
+
+ return bytes.TrimSpace(css)
+}
diff --git a/minify/css_minify_test.go b/minify/css_minify_test.go
new file mode 100644
index 00000000..0c22d356
--- /dev/null
+++ b/minify/css_minify_test.go
@@ -0,0 +1,32 @@
+package minify
+
+import (
+ "testing"
+
+ "github.com/gofiber/fiber/v2/utils"
+)
+
+func Test_Minify_cssMinify(t *testing.T) {
+ t.Parallel()
+ testCases := []struct {
+ name string
+ css []byte
+ expectedData string
+ }{
+ {
+ name: "Minify css",
+ css: filedata.css,
+ expectedData: expectedData.CSSMinify,
+ },
+ }
+ // loop through test cases
+ for _, tc := range testCases {
+ // run the test case with the given name
+ t.Run(tc.name, func(t *testing.T) {
+ // minify css
+ minifiedCss := cssMinify(tc.css)
+ // check if minified html equals expectedData
+ utils.AssertEqual(t, []byte(tc.expectedData), minifiedCss)
+ })
+ }
+}
diff --git a/minify/go.mod b/minify/go.mod
new file mode 100644
index 00000000..91125216
--- /dev/null
+++ b/minify/go.mod
@@ -0,0 +1,27 @@
+module github.com/gofiber/contrib/minify
+
+go 1.18
+
+require (
+ github.com/gofiber/fiber/v2 v2.44.0
+ golang.org/x/net v0.9.0
+ gopkg.in/yaml.v2 v2.4.0
+)
+
+require (
+ github.com/andybalholm/brotli v1.0.5 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/klauspost/compress v1.16.3 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.18 // indirect
+ github.com/mattn/go-runewidth v0.0.14 // indirect
+ github.com/philhofer/fwd v1.1.2 // indirect
+ github.com/rivo/uniseg v0.2.0 // indirect
+ github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
+ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
+ github.com/tinylib/msgp v1.1.8 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.45.0 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ golang.org/x/sys v0.7.0 // indirect
+)
diff --git a/minify/go.sum b/minify/go.sum
new file mode 100644
index 00000000..1605656c
--- /dev/null
+++ b/minify/go.sum
@@ -0,0 +1,86 @@
+github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
+github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8=
+github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
+github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
+github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
+github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
+github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
+github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
+github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
+github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
+github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
+github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
+github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA=
+github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/minify/html_minify.go b/minify/html_minify.go
new file mode 100644
index 00000000..b2cfa00e
--- /dev/null
+++ b/minify/html_minify.go
@@ -0,0 +1,128 @@
+package minify
+
+import (
+ "bytes"
+ "io"
+
+ "golang.org/x/net/html"
+)
+
+type Options struct {
+ MinifyScripts bool
+ MinifyStyles bool
+}
+
+// Minify returns minified version of the given HTML data.
+// If passed options is nil, uses default options.
+func htmlMinify(data []byte, options *Options) (out []byte, err error) {
+
+ var b bytes.Buffer
+ z := html.NewTokenizer(bytes.NewReader(data))
+ raw := 0
+ javascript := false
+ style := false
+ for {
+ tt := z.Next()
+ switch tt {
+ case html.ErrorToken:
+ err := z.Err()
+ if err == io.EOF {
+ return b.Bytes(), nil
+ }
+ return nil, err
+ case html.StartTagToken, html.SelfClosingTagToken:
+ tagName, hasAttr := z.TagName()
+ switch string(tagName) {
+ case "script":
+ javascript = true
+ raw++
+ case "style":
+ style = true
+ raw++
+ case "pre", "code", "textarea":
+ raw++
+ }
+ b.WriteByte('<')
+ b.Write(tagName)
+ var k, v []byte
+ isFirst := true
+ for hasAttr {
+ k, v, hasAttr = z.TagAttr()
+ if javascript && string(k) == "type" && string(v) != "text/javascript" {
+ javascript = false
+ }
+ if string(k) == "style" && options.MinifyStyles {
+ v = []byte("a{" + string(v) + "}") // simulate "full" CSS
+ v = cssMinify(v)
+ v = v[2 : len(v)-1] // strip simulation
+ }
+ if isFirst {
+ b.WriteByte(' ')
+ isFirst = false
+ }
+ b.Write(k)
+ if len(v) > 0 || isAlt(k) {
+ b.WriteByte('=')
+ qv := html.EscapeString(string(v))
+ // If the value is quoted with single quotes, replace them with double quotes.
+ b.WriteByte('"')
+ b.WriteString(qv)
+ b.WriteByte('"')
+ }
+ if hasAttr {
+ b.WriteByte(' ')
+ }
+ }
+ b.WriteByte('>')
+ case html.EndTagToken:
+ tagName, _ := z.TagName()
+ switch string(tagName) {
+ case "script":
+ javascript = false
+ raw--
+ case "style":
+ style = false
+ raw--
+ case "pre", "code", "textarea":
+ raw--
+ }
+ b.Write([]byte(""))
+ b.Write(tagName)
+ b.WriteByte('>')
+ case html.CommentToken:
+ if bytes.HasPrefix(z.Raw(), []byte("