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: Document

Hello, Fiber!


+html_with_css_default: >- + Document

Hello, Fiber!

+html_with_js_default: >- + Document

Hello, Fiber!

+html_with_css_and_js: >- + Document

Hello, Fiber!

+html_config_options_styles_false: >- + Document

Hello, Fiber!

+html_config_options_js_false: >- + Document

Hello, Fiber!

+html_config_options_css_js_false: >- + Document

Hello, Fiber!

+html_config_options_css_js_true: Document

Hello, 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("') + case html.CommentToken: + if bytes.HasPrefix(z.Raw(), []byte("