├── .github └── workflows │ ├── cli.yml │ ├── deno.yml │ ├── java.yml │ ├── nodejs.yml │ ├── python.yml │ ├── ruby.yml │ └── wasm.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── bench ├── README.md ├── build ├── fetch.js ├── graph.js ├── graphs │ ├── average-sizes.png │ ├── average-speeds.png │ ├── sizes.png │ └── speeds.png ├── inputs │ ├── Amazon │ ├── BBC │ ├── Bing │ ├── Bootstrap │ ├── Coding Horror │ ├── ECMA-262 │ ├── Google │ ├── Hacker News │ ├── NY Times │ ├── Reddit │ ├── Stack Overflow │ ├── Twitter │ └── Wikipedia ├── results.js ├── run └── runners │ ├── @minify-html%2Fnode │ ├── build │ ├── index.js │ ├── package.json │ └── run │ ├── README.md │ ├── common.js │ ├── html-minifier │ ├── build │ ├── index.js │ ├── package.json │ └── run │ ├── minify-html-onepass │ ├── Cargo.toml │ ├── build │ ├── run │ └── src │ │ └── main.rs │ ├── minify-html │ ├── Cargo.toml │ ├── build │ ├── run │ └── src │ │ └── main.rs │ ├── minimize │ ├── build │ ├── index.js │ ├── package.json │ └── run │ └── package.json ├── debug ├── diff │ ├── README.md │ ├── c14n │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ └── main.rs │ ├── canonicalise │ ├── charlines │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ └── main.rs │ ├── compare │ └── run └── prof │ ├── README.md │ └── profile.sh ├── format ├── icon ├── cli.png ├── deno.png ├── java.png ├── nodejs.png ├── python.png ├── ruby.png ├── rust.png └── wasm.png ├── minhtml ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── minify-html-common ├── Cargo.toml ├── build.rs ├── entities.json ├── html-data_2023013104.0.0.json └── src │ ├── gen │ ├── attrs.rs │ ├── codepoints.rs │ ├── entities.rs │ └── mod.rs │ ├── lib.rs │ ├── pattern.rs │ ├── spec │ ├── mod.rs │ ├── script.rs │ └── tag │ │ ├── mod.rs │ │ ├── ns.rs │ │ ├── omission.rs │ │ ├── void.rs │ │ └── whitespace.rs │ ├── tests │ └── mod.rs │ └── whitespace.rs ├── minify-html-java ├── Cargo.toml ├── pom.xml └── src │ └── main │ ├── java │ └── in │ │ └── wilsonl │ │ └── minifyhtml │ │ ├── Configuration.java │ │ ├── Configuration.java.gen.js │ │ └── MinifyHtml.java │ └── rust │ └── lib.rs ├── minify-html-nodejs ├── Cargo.toml ├── cli.js ├── index.d.ts ├── index.js ├── package.json ├── plat-pkg │ ├── README.md │ └── package.json.gen.js └── src │ └── lib.rs ├── minify-html-onepass-python ├── .cargo │ └── config ├── Cargo.toml ├── README.md ├── minify_html_onepass.pyi ├── pyproject.toml └── src │ └── lib.rs ├── minify-html-onepass ├── Cargo.toml ├── README.md └── src │ ├── cfg │ └── mod.rs │ ├── err.rs │ ├── lib.rs │ ├── proc │ ├── checkpoint.rs │ ├── entity.rs │ ├── mod.rs │ └── range.rs │ ├── tests │ └── mod.rs │ └── unit │ ├── attr │ ├── mod.rs │ └── value.rs │ ├── bang.rs │ ├── comment.rs │ ├── content.rs │ ├── instruction.rs │ ├── mod.rs │ ├── script.rs │ ├── style.rs │ └── tag.rs ├── minify-html-python ├── .cargo │ └── config ├── Cargo.toml ├── README.md ├── minify_html.pyi ├── pyproject.toml └── src │ └── lib.rs ├── minify-html-ruby ├── Gemfile ├── Rakefile ├── ext │ └── minify_html │ │ ├── Cargo.toml │ │ ├── extconf.rb │ │ └── src │ │ └── lib.rs ├── lib │ └── minify_html.rb └── minify_html.gemspec ├── minify-html-wasm ├── Cargo.toml ├── build ├── deno.json ├── package.merge.json └── src │ ├── lib.rs │ └── utils.rs ├── minify-html ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── ast │ ├── c14n.rs │ └── mod.rs │ ├── cfg │ └── mod.rs │ ├── entity │ ├── decode.rs │ ├── encode.rs │ ├── mod.rs │ └── tests │ │ ├── encode.rs │ │ └── mod.rs │ ├── lib.rs │ ├── minify │ ├── attr.rs │ ├── bang.rs │ ├── comment.rs │ ├── content.rs │ ├── css.rs │ ├── doctype.rs │ ├── element.rs │ ├── instruction.rs │ ├── js.rs │ ├── mod.rs │ ├── rcdata.rs │ └── tests │ │ ├── attr.rs │ │ └── mod.rs │ ├── parse │ ├── bang.rs │ ├── comment.rs │ ├── content.rs │ ├── doctype.rs │ ├── element.rs │ ├── instruction.rs │ ├── mod.rs │ ├── script.rs │ ├── style.rs │ ├── tests │ │ ├── element.rs │ │ └── mod.rs │ ├── textarea.rs │ └── title.rs │ ├── tag │ └── mod.rs │ └── tests │ └── mod.rs ├── notes ├── Parsing.md └── Script data.md └── version /.github/workflows/cli.yml: -------------------------------------------------------------------------------- 1 | name: Build and release CLI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | target: 15 | - aarch64-unknown-linux-gnu 16 | - x86_64-unknown-linux-gnu 17 | - aarch64-apple-darwin 18 | - x86_64-apple-darwin 19 | - x86_64-pc-windows-msvc 20 | include: 21 | - target: aarch64-unknown-linux-gnu 22 | os: ubuntu-20.04 23 | ext: '' 24 | - target: x86_64-unknown-linux-gnu 25 | os: ubuntu-20.04 26 | ext: '' 27 | - target: aarch64-apple-darwin 28 | os: macos-13 29 | ext: '' 30 | - target: x86_64-apple-darwin 31 | os: macos-13 32 | ext: '' 33 | - target: x86_64-pc-windows-msvc 34 | os: windows-2019 35 | ext: '.exe' 36 | steps: 37 | - uses: actions/checkout@v1 38 | 39 | - name: Get version 40 | id: version 41 | shell: bash 42 | run: echo ::set-output name=VERSION::"$([[ "$GITHUB_REF" == refs/tags/v* ]] && echo ${GITHUB_REF#refs/tags/v} || echo '0.0.0')" 43 | 44 | - name: Get file name 45 | id: file 46 | shell: bash 47 | run: echo ::set-output name=FILE::minhtml-${{ steps.version.outputs.VERSION }}-${{ matrix.target }}${{ matrix.ext }} 48 | 49 | - name: Set up Rust 50 | uses: dtolnay/rust-toolchain@stable 51 | with: 52 | targets: ${{ matrix.target }} 53 | 54 | - name: Set up cross compiler 55 | if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }} 56 | run: sudo apt install -y gcc-aarch64-linux-gnu 57 | 58 | - name: Build CLI 59 | working-directory: ./minhtml 60 | shell: bash 61 | run: | 62 | export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc 63 | cargo build --release --target ${{ matrix.target }} 64 | # upload-artifact does not rename the file, so when we download if we don't rename all files will have the same name. 65 | mv -v ../target/${{ matrix.target }}/release/minhtml${{ matrix.ext }} ${{ steps.file.outputs.FILE }} 66 | 67 | - name: Upload 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: ${{ steps.file.outputs.FILE }} 71 | path: minhtml/${{ steps.file.outputs.FILE }} 72 | 73 | release: 74 | runs-on: ubuntu-latest 75 | needs: build 76 | steps: 77 | - uses: actions/download-artifact@v4 78 | with: 79 | path: '.' 80 | merge-multiple: true 81 | 82 | - name: Release 83 | uses: softprops/action-gh-release@v1 84 | if: startsWith(github.ref, 'refs/tags/') 85 | with: 86 | files: '*' 87 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Deno package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | 13 | # https://jsr.io/docs/publishing-packages#publishing-from-github-actions 14 | permissions: 15 | contents: read 16 | id-token: write 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | 21 | - name: Get version 22 | id: version 23 | shell: bash 24 | run: echo ::set-output name=VERSION::"$([[ "$GITHUB_REF" == refs/tags/v* ]] && echo ${GITHUB_REF#refs/tags/v} || echo '0.0.0')" 25 | 26 | - name: Set up Rust 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | profile: minimal 31 | default: true 32 | 33 | - name: Install wasm-pack 34 | working-directory: ./minify-html-wasm 35 | shell: bash 36 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 37 | 38 | - name: Build native module 39 | working-directory: ./minify-html-wasm 40 | shell: bash 41 | run: TARGET=deno ./build 42 | 43 | - name: Publish to JSR 44 | working-directory: ./minify-html-wasm 45 | run: | 46 | # Sadly `jsr` ignores .gitignore files (i.e. our built dist package) and will fail on a dirty repo, 47 | # contrary to almost every other build/package system. 48 | # Therefore, we first move to a non-ignored folder. 49 | mv -iv pkg pkg-pub 50 | cd pkg-pub 51 | npx jsr publish --allow-dirty 52 | -------------------------------------------------------------------------------- /.github/workflows/java.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Java artifact 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | target: 15 | - aarch64-unknown-linux-gnu 16 | - x86_64-unknown-linux-gnu 17 | - aarch64-apple-darwin 18 | - x86_64-apple-darwin 19 | - x86_64-pc-windows-msvc 20 | include: 21 | - target: aarch64-unknown-linux-gnu 22 | os: ubuntu-20.04 23 | buildfile: 'libminify_html_java.so' 24 | resfile: 'linux-aarch64.nativelib' 25 | 26 | - target: x86_64-unknown-linux-gnu 27 | os: ubuntu-20.04 28 | buildfile: 'libminify_html_java.so' 29 | resfile: 'linux-x64.nativelib' 30 | 31 | - target: aarch64-apple-darwin 32 | os: macos-13 33 | buildfile: 'libminify_html_java.dylib' 34 | resfile: 'mac-aarch64.nativelib' 35 | 36 | - target: x86_64-apple-darwin 37 | os: macos-13 38 | buildfile: 'libminify_html_java.dylib' 39 | resfile: 'mac-x64.nativelib' 40 | 41 | - target: x86_64-pc-windows-msvc 42 | os: windows-2019 43 | buildfile: 'minify_html_java.dll' 44 | resfile: 'win-x64.nativelib' 45 | steps: 46 | - uses: actions/checkout@v1 47 | 48 | - name: Set up Rust 49 | uses: dtolnay/rust-toolchain@stable 50 | with: 51 | targets: ${{ matrix.target }} 52 | 53 | - name: Set up cross compiler 54 | if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }} 55 | run: sudo apt install -y gcc-aarch64-linux-gnu 56 | 57 | - name: Build Java native library 58 | working-directory: ./minify-html-java 59 | shell: bash 60 | run: | 61 | export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc 62 | cargo build --release --target ${{ matrix.target }} 63 | # upload-artifact does not rename the file, so when we download if we don't rename all files will have the same name. 64 | mv -v ../target/${{ matrix.target }}/release/${{ matrix.buildfile }} ${{ matrix.resfile }} 65 | 66 | - name: Upload built library 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: ${{ matrix.resfile }} 70 | path: minify-html-java/${{ matrix.resfile }} 71 | 72 | package: 73 | runs-on: ubuntu-20.04 74 | needs: build 75 | steps: 76 | - uses: actions/checkout@v1 77 | 78 | - name: Set up JDK 79 | uses: actions/setup-java@v1 80 | with: 81 | java-version: '8' 82 | java-package: jdk 83 | architecture: x64 84 | 85 | - uses: actions/download-artifact@v4 86 | with: 87 | path: 'minify-html-java/src/main/resources/' 88 | merge-multiple: true 89 | 90 | - name: Build, pack, and publish JAR 91 | working-directory: ./minify-html-java 92 | env: 93 | SONATYPE_GPG_PRIVATE_KEY: ${{ secrets.SONATYPE_GPG_PRIVATE_KEY }} 94 | run: | 95 | echo "$SONATYPE_GPG_PRIVATE_KEY" | gpg --allow-secret-key-import --import 96 | mkdir -p "$HOME/.m2" 97 | cat << 'EOF' > "$HOME/.m2/settings.xml" 98 | 99 | 100 | 101 | ossrh 102 | wilsonzlin 103 | ${{ secrets.SONATYPE_PASSWORD }} 104 | 105 | 106 | 107 | 108 | ossrh 109 | 110 | true 111 | 112 | 113 | 114 | 115 | EOF 116 | if [[ "$GITHUB_REF" == refs/tags/v* ]]; then 117 | mvn clean deploy 118 | fi 119 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Node.js package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | target: 15 | - aarch64-unknown-linux-gnu 16 | - x86_64-unknown-linux-gnu 17 | - aarch64-apple-darwin 18 | - x86_64-apple-darwin 19 | - x86_64-pc-windows-msvc 20 | include: 21 | - target: aarch64-unknown-linux-gnu 22 | os: ubuntu-20.04 23 | - target: x86_64-unknown-linux-gnu 24 | os: ubuntu-20.04 25 | - target: aarch64-apple-darwin 26 | os: macos-13 27 | - target: x86_64-apple-darwin 28 | os: macos-13 29 | - target: x86_64-pc-windows-msvc 30 | os: windows-2019 31 | steps: 32 | - uses: actions/checkout@v1 33 | 34 | - name: Set up Node.js 35 | uses: actions/setup-node@master 36 | with: 37 | node-version: 17.x 38 | 39 | - name: Set up Rust 40 | uses: dtolnay/rust-toolchain@stable 41 | with: 42 | targets: ${{ matrix.target }} 43 | 44 | - name: Set up cross compiler 45 | if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }} 46 | run: sudo apt install -y gcc-aarch64-linux-gnu 47 | 48 | - name: Build native module 49 | working-directory: ./minify-html-nodejs 50 | shell: bash 51 | run: | 52 | npm install 53 | export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc 54 | npm run build-release -- --target ${{ matrix.target }} 55 | mv -v index.node plat-pkg/. 56 | TARGET=${{ matrix.target }} node plat-pkg/package.json.gen.js 57 | 58 | - name: Create and publish native package 59 | # We need this fix: https://github.com/JS-DevTools/npm-publish/issues/198 60 | uses: JS-DevTools/npm-publish@66e0e1d9494ba904d4d608ae77fc5f4fe9bcc038 61 | if: startsWith(github.ref, 'refs/tags/v') 62 | with: 63 | access: public 64 | token: ${{ secrets.NPM_AUTH_TOKEN }} 65 | package: ./minify-html-nodejs/plat-pkg 66 | 67 | package: 68 | runs-on: ubuntu-latest 69 | needs: build 70 | steps: 71 | - uses: actions/checkout@v1 72 | 73 | - name: Prepare package 74 | working-directory: ./minify-html-nodejs 75 | run: | 76 | # npm refuses to work with symlinks. 77 | cp ../README.md . 78 | 79 | - name: Pack and publish package 80 | uses: JS-DevTools/npm-publish@v3 81 | if: startsWith(github.ref, 'refs/tags/v') 82 | with: 83 | access: public 84 | token: ${{ secrets.NPM_AUTH_TOKEN }} 85 | package: ./minify-html-nodejs 86 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | # How cross compilation works with PyO3/maturin-action: 10 | # - For Linux targets, it uses Docker. 11 | # - For other targets, it runs on the host (Docker mostly only supports Linux containers). 12 | # - Even if targeting another platform (e.g. Windows from Linux) was possible, this is likely problematic and suboptimal: 13 | # - Working with libraries and headers for another OS is likely very tricky. 14 | # - Usually the host compilers (e.g. Xcode on macOS, MSVC on Windows) are the most optimal in terms of correctness and performance. 15 | # - It may be against Apple's terms to build macOS binaries from other platforms. 16 | # - On Windows, the action appears to just use the host binary. Cross compilation to a different arch doesn't seem to be yet supported. 17 | # - On macOS, the action appears to be able to target aarch64 just fine despite running on an Intel Mac. 18 | # - We don't need to set up Rust, the action will do so itself. 19 | # - We don't need to set up Python: 20 | # - On macOS, the action auto finds all versions: https://github.com/PyO3/maturin-action/blob/a3013db91b2ef2e51420cfe99ee619c8e72a17e6/src/index.ts#L732 21 | # - On Linux, a Docker container is used which has its own Rust, Python, etc. 22 | # - On Windows, we use the "generate-import-lib" feature on pyo3: https://www.maturin.rs/distribution#cross-compile-to-windows 23 | jobs: 24 | build: 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | os: [ubuntu-latest, macos-13, windows-2019] 29 | variant: ['', '-onepass'] 30 | target: [x86_64, aarch64] 31 | python: 32 | - '3.8' 33 | - '3.9' 34 | - '3.10' 35 | - '3.11' 36 | - '3.12' 37 | - '3.13' 38 | exclude: 39 | # Building for Windows aarch64 isn't ready yet. 40 | - os: windows-2019 41 | target: aarch64 42 | steps: 43 | - uses: actions/checkout@v1 44 | 45 | - name: Patches 46 | shell: bash 47 | run: | 48 | # Maturin uses Cargo.toml name for Python name: https://www.maturin.rs/#usage. 49 | # We edit it here instead of just updating our repo code as Cargo workspace names must be different. 50 | newName=minify-html${{ matrix.variant }} 51 | newName=${newName//-/_} 52 | sed -i.bak 's%^name = ".*$%name = "'$newName'"%' ./minify-html${{ matrix.variant }}-python/Cargo.toml 53 | rm ./minify-html${{ matrix.variant }}-python/Cargo.toml.bak 54 | 55 | - name: Build wheels 56 | uses: PyO3/maturin-action@v1 57 | with: 58 | target: ${{ matrix.target }} 59 | manylinux: auto 60 | # https://github.com/PyO3/maturin-action/issues/49#issuecomment-1166242843 61 | args: --release --sdist --strip -m ./minify-html${{ matrix.variant }}-python/Cargo.toml -i ${{ matrix.python }} 62 | sccache: true 63 | 64 | - name: Install Python build tools (macOS) 65 | if: runner.os == 'macOS' 66 | run: sudo pip install --upgrade twine 67 | - name: Install Python build tools (Linux, Windows) 68 | if: runner.os != 'macOS' 69 | run: pip install --upgrade twine 70 | 71 | - name: Pack and publish package 72 | shell: bash 73 | working-directory: ./minify-html${{ matrix.variant }}-python 74 | run: | 75 | cat << 'EOF' > "$HOME/.pypirc" 76 | [pypi] 77 | username = __token__ 78 | password = ${{ secrets.PYPI_API_TOKEN }} 79 | EOF 80 | if [[ "$GITHUB_REF" == refs/tags/v* ]]; then 81 | # For idempotency, ignore any existing built wheels that have already been successfully uploaded. 82 | twine upload --skip-existing ../target/wheels/* 83 | else 84 | ls -al ../target/wheels/* 85 | fi 86 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Ruby gem 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | # https://github.com/oxidize-rb/actions/tree/main/cross-gem 10 | # https://github.com/gjtorikian/commonmarker/blob/main/.github/workflows/tag_and_release.yml 11 | # https://github.com/yettoapp/actions/blob/main/.github/workflows/ruby_gem_release.yml 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-20.04 16 | strategy: 17 | matrix: 18 | platform: 19 | - x86_64-linux 20 | - aarch64-linux 21 | - x86_64-darwin 22 | - arm64-darwin 23 | - x64-mingw-ucrt 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - name: Patches 28 | run: | 29 | # Name must match or else `RbSys::PackageNotFoundError: Could not find Cargo package metadata for nil. Please check that nil matches the crate name in your Cargo.toml.` 30 | # We edit it here instead of just updating our repo code as Cargo workspace names must be different. 31 | sed -i 's%^name = ".*$%name = "minify_html"%' ./minify-html-ruby/ext/minify_html/Cargo.toml 32 | 33 | # rb-sys-dock doesn't support local path deps: https://github.com/oxidize-rb/rb-sys/issues/296 34 | ver=$(jq -r .version minify-html-nodejs/package.json) 35 | sed -i 's%^minify-html = .*$%minify-html = "'$ver'"%' ./minify-html-ruby/ext/minify_html/Cargo.toml 36 | 37 | # This is required or else `error: could not find `Cargo.toml` in `/home/runner/work/.../minify-html-ruby` or any parent directory`. 38 | cat <<'EOD'>./minify-html-ruby/Cargo.toml 39 | [workspace] 40 | members = ["ext/minify_html"] 41 | EOD 42 | 43 | cp ./README.md ./minify-html-ruby/. 44 | 45 | - name: Set up Ruby and Rust 46 | uses: oxidize-rb/actions/setup-ruby-and-rust@main 47 | with: 48 | ruby-version: "3.4" 49 | bundler-cache: false 50 | cargo-cache: true 51 | cargo-vendor: true 52 | cache-version: v0-${{ matrix.platform }} 53 | 54 | - name: Cross compile 55 | id: gem 56 | uses: oxidize-rb/actions/cross-gem@v1 57 | with: 58 | platform: ${{ matrix.platform }} 59 | ruby-versions: "2.7,3.0,3.1,3.2,3.3,3.4" 60 | working-directory: ./minify-html-ruby 61 | 62 | - name: Publish gem 63 | if: startsWith(github.ref, 'refs/tags/v') 64 | run: | 65 | mkdir -p "$HOME/.gem" 66 | cat << 'EOF' > "$HOME/.gem/credentials" 67 | --- 68 | :rubygems_api_key: ${{ secrets.RUBYGEMS_API_KEY }} 69 | EOF 70 | chmod 0600 "$HOME/.gem/credentials" 71 | gem push ${{ steps.gem.outputs.gem-path }} 72 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish WASM package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | - name: Get version 16 | id: version 17 | shell: bash 18 | run: echo ::set-output name=VERSION::"$([[ "$GITHUB_REF" == refs/tags/v* ]] && echo ${GITHUB_REF#refs/tags/v} || echo '0.0.0')" 19 | 20 | - name: Set up Rust 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | profile: minimal 25 | default: true 26 | 27 | - name: Install wasm-pack 28 | working-directory: ./minify-html-wasm 29 | shell: bash 30 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 31 | 32 | - name: Build native module 33 | working-directory: ./minify-html-wasm 34 | shell: bash 35 | run: TARGET=bundler ./build 36 | 37 | - name: Set up Node.js 38 | uses: actions/setup-node@master 39 | with: 40 | node-version: 17.x 41 | 42 | - name: Pack and publish package 43 | working-directory: ./minify-html-wasm/pkg 44 | run: | 45 | cat << 'EOF' > .npmrc 46 | package-lock=false 47 | //registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }} 48 | EOF 49 | # npm refuses to work with symlinks. 50 | cp ../../README.md . 51 | if [[ "${{ steps.version.outputs.VERSION }}" != "0.0.0" ]]; then 52 | npm publish --access public 53 | fi 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | bin/ 3 | build/ 4 | Cargo.lock 5 | dist/ 6 | Gemfile.lock 7 | node_modules/ 8 | npm-debug.log* 9 | out/ 10 | package-lock.json 11 | perf.data* 12 | target/ 13 | tmp/ 14 | wasm-pack.log 15 | 16 | # Bench 17 | /bench/results/ 18 | 19 | # Debug 20 | /debug/diff/outputs/ 21 | 22 | # Java 23 | *.nativelib 24 | 25 | # Node.js 26 | index.node 27 | 28 | # Python 29 | *.egg-info/ 30 | 31 | # WASM 32 | /minify-html-wasm/pkg/ 33 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | fn_single_line = false 2 | force_explicit_abi = true 3 | format_macro_bodies = true 4 | format_macro_matchers = true 5 | group_imports = "One" 6 | hard_tabs = false 7 | hex_literal_case = "Lower" 8 | imports_granularity = "Item" 9 | imports_layout = "Horizontal" 10 | merge_derives = true 11 | overflow_delimited_expr = true 12 | remove_nested_parens = true 13 | reorder_impl_items = true 14 | reorder_imports = true 15 | reorder_modules = true 16 | tab_spaces = 2 17 | trailing_semicolon = true 18 | use_field_init_shorthand = true 19 | wrap_comments = false 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "bench/runners/minify-html-onepass", 4 | "bench/runners/minify-html", 5 | "debug/diff/c14n", 6 | "debug/diff/charlines", 7 | "minhtml", 8 | "minify-html-common", 9 | "minify-html-java", 10 | "minify-html-nodejs", 11 | "minify-html-onepass-python", 12 | "minify-html-onepass", 13 | "minify-html-python", 14 | "minify-html-ruby/ext/minify_html", 15 | "minify-html-wasm", 16 | "minify-html", 17 | ] 18 | 19 | [profile.release] 20 | codegen-units = 1 21 | lto = true 22 | opt-level = 3 23 | strip = true 24 | 25 | [profile.release.package."*"] 26 | codegen-units = 1 27 | opt-level = 3 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wilson Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bench/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarking 2 | 3 | This folder contains scripts used to test the performance and effectiveness of minify-html, for guided optimisation and/or comparisons. 4 | 5 | It also contains a set of common web pages as inputs for benchmarking. 6 | 7 | ## Comparison 8 | 9 | Each minifier is run against each file in the [inputs](./inputs) folder, which are HTML pages fetched from popular websites: 10 | 11 | |File name|URL| 12 | |---|---| 13 | |Amazon|https://www.amazon.com/| 14 | |BBC|https://www.bbc.co.uk/| 15 | |Bootstrap|https://getbootstrap.com/docs/3.4/css/| 16 | |Bing|https://www.bing.com/| 17 | |Coding Horror|https://blog.codinghorror.com/| 18 | |ECMA-262|https://www.ecma-international.org/ecma-262/10.0/index.html| 19 | |Google|https://www.google.com/| 20 | |Hacker News|https://news.ycombinator.com/| 21 | |NY Times|https://www.nytimes.com/| 22 | |Reddit|https://www.reddit.com/| 23 | |Stack Overflow|https://www.stackoverflow.com/| 24 | |Twitter|https://twitter.com/| 25 | |Wikipedia|https://en.wikipedia.org/wiki/Soil| 26 | 27 | **Note that these pages are already mostly minified.** 28 | 29 | For more information on how the inputs are fetched, see [fetch.js](./fetch.js). 30 | 31 | On this [project's README](../README.md), average graphs are shown. Graphs showing per-input results are shown below: 32 | 33 | Chart showing speed of HTML minifiers per inputChart showing effectiveness of HTML minifiers per input 34 | 35 | Results depend on the input, so charts show performance relative to minify-html as a percentage. 36 | 37 | ## Running 38 | 39 | Run [build](./build) to build the minifiers. 40 | 41 | Run [run](./run) to benchmark each HTML minifier against each input and output the results to the `results` folder. 42 | 43 | Run [graph.js](./graph.js) to render graphs to the `graphs` folder. 44 | -------------------------------------------------------------------------------- /bench/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuo pipefail 4 | 5 | shopt -s nullglob 6 | 7 | pushd "$(dirname "$0")" >/dev/null 8 | 9 | pushd runners >/dev/null 10 | npm i 11 | 12 | for r in *; do 13 | if [[ ! -d "$r" ]] || [[ "$r" == "node_modules" ]]; then 14 | continue 15 | fi 16 | echo "Building $r..." 17 | pushd "$r" >/dev/null 18 | ./build 19 | popd >/dev/null 20 | done 21 | popd >/dev/null 22 | 23 | echo "All done!" 24 | 25 | popd >/dev/null 26 | -------------------------------------------------------------------------------- /bench/fetch.js: -------------------------------------------------------------------------------- 1 | const { promises: fs } = require("fs"); 2 | const childProcess = require("child_process"); 3 | const path = require("path"); 4 | 5 | const tests = { 6 | Amazon: "https://www.amazon.com/", 7 | BBC: "https://www.bbc.co.uk/", 8 | Bootstrap: "https://getbootstrap.com/docs/3.4/css/", 9 | Bing: "https://www.bing.com/", 10 | "Coding Horror": "https://blog.codinghorror.com/", 11 | "ECMA-262": "https://www.ecma-international.org/ecma-262/10.0/index.html", 12 | Google: "https://www.google.com/", 13 | "Hacker News": "https://news.ycombinator.com/", 14 | "NY Times": "https://www.nytimes.com/", 15 | Reddit: "https://www.reddit.com/", 16 | "Stack Overflow": "https://www.stackoverflow.com/", 17 | Twitter: "https://twitter.com/", 18 | Wikipedia: "https://en.wikipedia.org/wiki/Soil", 19 | }; 20 | 21 | const fetchTest = (name, url) => 22 | new Promise((resolve, reject) => { 23 | // Use curl to follow redirects without needing a Node.js library. 24 | childProcess.execFile( 25 | "curl", 26 | [ 27 | "-H", 28 | `User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; rv:71.0) Gecko/20100101 Firefox/71.0`, 29 | "-H", 30 | "Accept: */*", 31 | "-fLSs", 32 | url, 33 | ], 34 | (error, stdout, stderr) => { 35 | if (error) { 36 | return reject(error); 37 | } 38 | if (stderr) { 39 | return reject(new Error(`stderr: ${stderr}`)); 40 | } 41 | resolve([name, stdout]); 42 | } 43 | ); 44 | }); 45 | 46 | (async () => { 47 | const existing = await fs.readdir(path.join(__dirname, "tests")); 48 | await Promise.all( 49 | existing.map((e) => fs.unlink(path.join(__dirname, "tests", e))) 50 | ); 51 | 52 | // Format after fetching as formatting is synchronous and can take so long that connections get dropped by server due to inactivity. 53 | for (const [name, html] of await Promise.all( 54 | Object.entries(tests).map(([name, url]) => fetchTest(name, url)) 55 | )) { 56 | // Apply some fixes to HTML. 57 | const fixed = html 58 | // Fix early termination of conditional comment in Amazon. 59 | .replace("-->\n", "\n") 60 | // Fix closing of void tag in Amazon. 61 | .replace(/><\/hr>/g, "/>") 62 | // Fix extra '' in BBC. 63 | .replace( 64 | "", 65 | "" 66 | ) 67 | // Fix broken attribute value in Stack Overflow. 68 | .replace('height=151"', 'height="151"'); 69 | await fs.writeFile(path.join(__dirname, "tests", name), fixed); 70 | } 71 | })().catch(console.error); 72 | -------------------------------------------------------------------------------- /bench/graph.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs/promises"); 2 | const https = require("https"); 3 | const path = require("path"); 4 | const results = require("./results"); 5 | 6 | const GRAPHS_DIR = path.join(__dirname, "graphs"); 7 | const SPEEDS_GRAPH = path.join(GRAPHS_DIR, "speeds.png"); 8 | const SIZES_GRAPH = path.join(GRAPHS_DIR, "sizes.png"); 9 | const AVERAGE_SPEEDS_GRAPH = path.join(GRAPHS_DIR, "average-speeds.png"); 10 | const AVERAGE_SIZES_GRAPH = path.join(GRAPHS_DIR, "average-sizes.png"); 11 | 12 | const speedColours = { 13 | "minify-html": "#2e61bd", 14 | "minify-html-onepass": "#222", 15 | }; 16 | const defaultSpeedColour = "rgb(188, 188, 188)"; 17 | 18 | const sizeColours = { 19 | "minify-html": "#2e61bd", 20 | }; 21 | const defaultSizeColour = "rgb(188, 188, 188)"; 22 | 23 | const averageChartOptions = (label) => ({ 24 | options: { 25 | legend: { 26 | display: false, 27 | }, 28 | scales: { 29 | xAxes: [ 30 | { 31 | barPercentage: 0.5, 32 | gridLines: { 33 | display: false, 34 | }, 35 | ticks: { 36 | fontColor: "#555", 37 | fontSize: 20, 38 | }, 39 | }, 40 | ], 41 | yAxes: [ 42 | { 43 | type: "linear", 44 | scaleLabel: { 45 | display: true, 46 | fontColor: "#222", 47 | fontSize: 24, 48 | fontStyle: "bold", 49 | labelString: label, 50 | padding: 12, 51 | }, 52 | position: "left", 53 | ticks: { 54 | callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$", 55 | fontColor: "#222", 56 | fontSize: 20, 57 | }, 58 | gridLines: { 59 | color: "#eee", 60 | }, 61 | }, 62 | ], 63 | }, 64 | }, 65 | }); 66 | 67 | const breakdownChartOptions = (title) => ({ 68 | options: { 69 | legend: { 70 | display: true, 71 | labels: { 72 | fontColor: "#000", 73 | fontSize: 20, 74 | }, 75 | }, 76 | title: { 77 | display: true, 78 | text: title, 79 | fontColor: "#333", 80 | fontSize: 24, 81 | }, 82 | scales: { 83 | xAxes: [ 84 | { 85 | gridLines: { 86 | color: "#f2f2f2", 87 | }, 88 | ticks: { 89 | callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$", 90 | fontColor: "#999", 91 | fontSize: 20, 92 | }, 93 | }, 94 | ], 95 | yAxes: [ 96 | { 97 | barPercentage: 0.5, 98 | gridLines: { 99 | color: "#aaa", 100 | }, 101 | ticks: { 102 | fontColor: "#666", 103 | fontSize: 20, 104 | }, 105 | }, 106 | ], 107 | }, 108 | }, 109 | }); 110 | 111 | const renderChart = (cfg, width, height) => 112 | new Promise((resolve, reject) => { 113 | const req = https.request("https://quickchart.io/chart", { 114 | method: "POST", 115 | headers: { 116 | "Content-Type": "application/json", 117 | }, 118 | }); 119 | req.on("error", reject); 120 | req.on("response", (res) => { 121 | const err = res.headers["x-quickchart-error"]; 122 | if (res.statusCode < 200 || res.statusCode > 299 || err) { 123 | return reject(new Error(err || `Status ${res.statusCode}`)); 124 | } 125 | const chunks = []; 126 | res.on("error", reject); 127 | res.on("data", (c) => chunks.push(c)); 128 | res.on("end", () => resolve(Buffer.concat(chunks))); 129 | }); 130 | req.end( 131 | JSON.stringify({ 132 | backgroundColor: "white", 133 | chart: JSON.stringify(cfg).replaceAll( 134 | '"$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$"', 135 | "function(value) {return Math.round(value * 10000) / 100 + '%';}" 136 | ), 137 | width, 138 | height, 139 | format: "png", 140 | }) 141 | ); 142 | }); 143 | 144 | (async () => { 145 | await fs.mkdir(GRAPHS_DIR, { recursive: true }); 146 | 147 | const res = results.calculate(); 148 | const speedMinifiers = [ 149 | "html-minifier", 150 | "minimize", 151 | "minify-html", 152 | "minify-html-onepass", 153 | ]; 154 | const sizeMinifiers = ["minimize", "html-minifier", "minify-html"]; 155 | const inputs = Object.keys(res.inputSizes).sort(); 156 | 157 | await fs.writeFile( 158 | AVERAGE_SPEEDS_GRAPH, 159 | await renderChart( 160 | { 161 | type: "bar", 162 | data: { 163 | labels: speedMinifiers, 164 | datasets: [ 165 | { 166 | backgroundColor: speedMinifiers.map( 167 | (n) => speedColours[n] ?? defaultSpeedColour 168 | ), 169 | data: speedMinifiers.map( 170 | (m) => res.minifierAvgOps[m] / res.maxMinifierAvgOps 171 | ), 172 | }, 173 | ], 174 | }, 175 | ...averageChartOptions("Performance"), 176 | }, 177 | 1024, 178 | 768 179 | ) 180 | ); 181 | 182 | await fs.writeFile( 183 | AVERAGE_SIZES_GRAPH, 184 | await renderChart( 185 | { 186 | type: "bar", 187 | data: { 188 | labels: sizeMinifiers, 189 | datasets: [ 190 | { 191 | backgroundColor: sizeMinifiers.map( 192 | (n) => sizeColours[n] ?? defaultSizeColour 193 | ), 194 | data: sizeMinifiers.map((m) => res.minifierAvgReduction[m]), 195 | }, 196 | ], 197 | }, 198 | ...averageChartOptions("Reduction"), 199 | }, 200 | 1024, 201 | 768 202 | ) 203 | ); 204 | 205 | await fs.writeFile( 206 | SPEEDS_GRAPH, 207 | await renderChart( 208 | { 209 | type: "horizontalBar", 210 | data: { 211 | labels: inputs, 212 | datasets: speedMinifiers.map((minifier) => ({ 213 | label: minifier, 214 | data: inputs.map( 215 | (input) => 216 | res.perInputOps[minifier][input] / 217 | res.perInputOps["minify-html"][input] 218 | ), 219 | })), 220 | }, 221 | ...breakdownChartOptions( 222 | "Operations per second, relative to minify-html" 223 | ), 224 | }, 225 | 900, 226 | 1600 227 | ) 228 | ); 229 | 230 | await fs.writeFile( 231 | SIZES_GRAPH, 232 | await renderChart( 233 | { 234 | type: "horizontalBar", 235 | data: { 236 | labels: inputs, 237 | datasets: sizeMinifiers.map((minifier) => ({ 238 | label: minifier, 239 | data: inputs.map( 240 | (input) => 241 | res.perInputReduction[minifier][input] / 242 | res.perInputReduction["minify-html"][input] 243 | ), 244 | })), 245 | }, 246 | ...breakdownChartOptions("Size reduction, relative to minify-html"), 247 | }, 248 | 900, 249 | 1600 250 | ) 251 | ); 252 | })(); 253 | -------------------------------------------------------------------------------- /bench/graphs/average-sizes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/bench/graphs/average-sizes.png -------------------------------------------------------------------------------- /bench/graphs/average-speeds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/bench/graphs/average-speeds.png -------------------------------------------------------------------------------- /bench/graphs/sizes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/bench/graphs/sizes.png -------------------------------------------------------------------------------- /bench/graphs/speeds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/bench/graphs/speeds.png -------------------------------------------------------------------------------- /bench/results.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const RESULTS_DIR = path.join(__dirname, "results"); 5 | const INPUTS_DIR = path.join(__dirname, "inputs"); 6 | 7 | module.exports = { 8 | calculate: () => { 9 | // minifier => avg(ops). 10 | const minifierAvgOps = {}; 11 | // minifier => avg(1 - output / original). 12 | const minifierAvgReduction = {}; 13 | let maxMinifierAvgOps = 0; 14 | // minifier => input => ops. 15 | const perInputOps = {}; 16 | // minifier => input => (1 - output / original). 17 | const perInputReduction = {}; 18 | // input => max(ops). 19 | const maxInputOps = {}; 20 | const inputSizes = Object.fromEntries( 21 | fs.readdirSync(INPUTS_DIR).map((f) => { 22 | const name = path.basename(f, ".json"); 23 | const stats = fs.statSync(path.join(INPUTS_DIR, f)); 24 | return [name, stats.size]; 25 | }) 26 | ); 27 | 28 | for (const f of fs.readdirSync(RESULTS_DIR)) { 29 | const minifier = decodeURIComponent(path.basename(f, ".json")); 30 | const data = JSON.parse( 31 | fs.readFileSync(path.join(RESULTS_DIR, f), "utf8") 32 | ); 33 | for (const [input, size, iterations, seconds] of data) { 34 | const originalSize = inputSizes[input]; 35 | const ops = 1 / (seconds / iterations); 36 | const reduction = 1 - size / originalSize; 37 | (minifierAvgOps[minifier] ??= []).push(ops); 38 | (minifierAvgReduction[minifier] ??= []).push(reduction); 39 | (perInputOps[minifier] ??= {})[input] = ops; 40 | (perInputReduction[minifier] ??= {})[input] = reduction; 41 | maxInputOps[input] = Math.max(maxInputOps[input] ?? 0, ops); 42 | } 43 | } 44 | 45 | const minifiers = Object.keys(minifierAvgOps); 46 | for (const m of minifiers) { 47 | minifierAvgOps[m] = 48 | minifierAvgOps[m].reduce((sum, ops) => sum + ops, 0) / 49 | minifierAvgOps[m].length; 50 | maxMinifierAvgOps = Math.max(maxMinifierAvgOps, minifierAvgOps[m]); 51 | minifierAvgReduction[m] = 52 | minifierAvgReduction[m].reduce((sum, ops) => sum + ops, 0) / 53 | minifierAvgReduction[m].length; 54 | } 55 | 56 | return { 57 | minifierAvgReduction, 58 | minifierAvgOps, 59 | maxMinifierAvgOps, 60 | perInputOps, 61 | perInputReduction, 62 | maxInputOps, 63 | inputSizes, 64 | minifiers, 65 | }; 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /bench/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuo pipefail 4 | 5 | shopt -s nullglob 6 | 7 | pushd "$(dirname "$0")" >/dev/null 8 | 9 | for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do 10 | echo performance | sudo dd status=none of="$i" 11 | done 12 | 13 | results_dir="$PWD/results" 14 | input_dir="$PWD/inputs" 15 | iterations=${MHB_ITERATIONS:-25} 16 | 17 | mkdir -p "$results_dir" 18 | 19 | pushd runners >/dev/null 20 | for r in *; do 21 | if [[ ! -d "$r" ]] || [[ "$r" == "node_modules" ]]; then 22 | continue 23 | fi 24 | echo "Running $r..." 25 | pushd "$r" >/dev/null 26 | out="$results_dir/$r.json" 27 | if [[ "$(uname -s)" = "Darwin" ]]; then 28 | MHB_ITERATIONS="$iterations" MHB_INPUT_DIR="$input_dir" RUST_BACKTRACE=1 ./run >"$out" 29 | else 30 | sudo --preserve-env=MHB_HTML_ONLY,PATH MHB_ITERATIONS="$iterations" MHB_INPUT_DIR="$input_dir" RUST_BACKTRACE=1 nice -n -20 taskset -c 1 ./run >"$out" 31 | fi 32 | popd >/dev/null 33 | done 34 | popd >/dev/null 35 | 36 | echo "All done!" 37 | 38 | popd >/dev/null 39 | -------------------------------------------------------------------------------- /bench/runners/@minify-html%2Fnode/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | pushd ../../../minify-html-nodejs 6 | # Ensure devdeps required for building are installed. 7 | npm i 8 | npm run build 9 | popd 10 | 11 | npm i 12 | -------------------------------------------------------------------------------- /bench/runners/@minify-html%2Fnode/index.js: -------------------------------------------------------------------------------- 1 | const minifyHtml = require("@minify-html/node"); 2 | const { htmlOnly, run } = require("../common"); 3 | 4 | const minifyHtmlCfg = { 5 | minify_css: !htmlOnly, 6 | minify_js: !htmlOnly, 7 | allow_noncompliant_unquoted_attribute_values: true, 8 | allow_optimal_entities: true, 9 | allow_removing_spaces_between_attributes: true, 10 | minify_doctype: true, 11 | }; 12 | 13 | run((src) => minifyHtml.minify(src, minifyHtmlCfg)); 14 | -------------------------------------------------------------------------------- /bench/runners/@minify-html%2Fnode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "@minify-html/node": "file:../../../minify-html-nodejs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /bench/runners/@minify-html%2Fnode/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | node index.js 6 | -------------------------------------------------------------------------------- /bench/runners/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark runners 2 | 3 | - Each directory should have an executable `./build` and `./run`. 4 | - The runners should use the following environment variables: 5 | - `MHB_ITERATIONS`: times to run each input. 6 | - `MHB_INPUT_DIR`: path to directory containing inputs. Files should be read from this directory and used as the inputs. 7 | - `MHB_HTML_ONLY`: if set to `1`, `minify_css` and `minify_js` should be disabled. 8 | - `MHB_OUTPUT_DIR`: if set, output minified HTML for each input in this folder. 9 | - The output should be a JSON array of tuples, where each tuples contains the input name, output UTF-8 byte length, iterations, and execution time in seconds (as a floating point value). 10 | - The execution time should be measured using high-precision monotonic system clocks where possible. 11 | -------------------------------------------------------------------------------- /bench/runners/common.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | 5 | const iterations = parseInt(process.env.MHB_ITERATIONS, 10); 6 | const inputDir = process.env.MHB_INPUT_DIR; 7 | const htmlOnly = process.env.MHB_HTML_ONLY === "1"; 8 | const outputDir = process.env.MHB_OUTPUT_DIR; 9 | 10 | module.exports = { 11 | htmlOnly, 12 | 13 | esbuildCss: (code, type) => { 14 | if (type === "inline") { 15 | code = `x{${code}}`; 16 | } 17 | code = esbuild.transformSync(code, { 18 | charset: "utf8", 19 | legalComments: "none", 20 | loader: "css", 21 | minify: true, 22 | sourcemap: false, 23 | }).code; 24 | if (type === "inline") { 25 | code = code.trim().slice(2, -1); 26 | } 27 | return code; 28 | }, 29 | 30 | esbuildJs: (code) => 31 | esbuild.transformSync(code, { 32 | charset: "utf8", 33 | legalComments: "none", 34 | loader: "js", 35 | minify: true, 36 | sourcemap: false, 37 | }).code, 38 | 39 | run: (minifierFn) => { 40 | console.log( 41 | JSON.stringify( 42 | fs.readdirSync(inputDir).map((name) => { 43 | const src = fs.readFileSync(path.join(inputDir, name)); 44 | 45 | const out = minifierFn(src); 46 | const len = Buffer.from(out).length; 47 | if (outputDir) { 48 | fs.writeFileSync(path.join(outputDir, name), out); 49 | } 50 | 51 | const start = process.hrtime.bigint(); 52 | for (let i = 0; i < iterations; i++) { 53 | minifierFn(src); 54 | } 55 | const elapsed = process.hrtime.bigint() - start; 56 | 57 | return [name, len, iterations, Number(elapsed) / 1_000_000_000]; 58 | }) 59 | ) 60 | ); 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /bench/runners/html-minifier/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | npm i 6 | -------------------------------------------------------------------------------- /bench/runners/html-minifier/index.js: -------------------------------------------------------------------------------- 1 | const htmlMinifier = require("html-minifier"); 2 | const { htmlOnly, esbuildCss, esbuildJs, run } = require("../common"); 3 | 4 | const htmlMinifierCfg = { 5 | collapseBooleanAttributes: true, 6 | collapseInlineTagWhitespace: true, 7 | collapseWhitespace: true, 8 | // minify-html can do context-aware whitespace removal, which is safe when configured correctly to match how whitespace is used in the document. 9 | // html-minifier cannot, so whitespace must be collapsed conservatively. 10 | // Alternatively, minify-html can also be made to remove whitespace regardless of context. 11 | conservativeCollapse: true, 12 | customEventAttributes: [], 13 | decodeEntities: true, 14 | ignoreCustomComments: [], 15 | ignoreCustomFragments: [/<\?[\s\S]*?\?>/], 16 | minifyCSS: !htmlOnly && esbuildCss, 17 | minifyJS: !htmlOnly && esbuildJs, 18 | processConditionalComments: true, 19 | removeAttributeQuotes: true, 20 | removeComments: true, 21 | removeEmptyAttributes: true, 22 | removeOptionalTags: true, 23 | removeRedundantAttributes: true, 24 | removeScriptTypeAttributes: true, 25 | removeStyleLinkTypeAttributes: true, 26 | removeTagWhitespace: true, 27 | useShortDoctype: true, 28 | }; 29 | 30 | run((src) => htmlMinifier.minify(src.toString(), htmlMinifierCfg)); 31 | -------------------------------------------------------------------------------- /bench/runners/html-minifier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "html-minifier": "4.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /bench/runners/html-minifier/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | node index.js 6 | -------------------------------------------------------------------------------- /bench/runners/minify-html-onepass/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minify-html-onepass-bench" 3 | publish = false 4 | version = "0.0.1" 5 | authors = ["Wilson Lin "] 6 | edition = "2018" 7 | 8 | [dependencies] 9 | minify-html-onepass = { path = "../../../minify-html-onepass" } 10 | serde = { version = "1.0.104", features = ["derive"] } 11 | serde_json = "1.0.44" 12 | -------------------------------------------------------------------------------- /bench/runners/minify-html-onepass/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | cargo build --release 6 | -------------------------------------------------------------------------------- /bench/runners/minify-html-onepass/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | ../../../target/release/minify-html-onepass-bench 6 | -------------------------------------------------------------------------------- /bench/runners/minify-html-onepass/src/main.rs: -------------------------------------------------------------------------------- 1 | use minify_html_onepass::in_place; 2 | use minify_html_onepass::Cfg; 3 | use std::env; 4 | use std::fs; 5 | use std::io::stdout; 6 | use std::time::Instant; 7 | 8 | fn main() { 9 | let iterations = env::var("MHB_ITERATIONS") 10 | .unwrap() 11 | .parse::() 12 | .unwrap(); 13 | let input_dir = env::var("MHB_INPUT_DIR").unwrap(); 14 | let html_only = env::var("MHB_HTML_ONLY") 15 | .ok() 16 | .filter(|v| v == "1") 17 | .is_some(); 18 | let output_dir = env::var("MHB_OUTPUT_DIR").ok(); 19 | 20 | let mut results: Vec<(String, usize, usize, f64)> = Vec::new(); 21 | let cfg = Cfg { 22 | minify_css: !html_only, 23 | minify_js: !html_only, 24 | }; 25 | 26 | for t in fs::read_dir(input_dir).unwrap().map(|d| d.unwrap()) { 27 | let source = fs::read(t.path()).unwrap(); 28 | let input_name = t.file_name().into_string().unwrap(); 29 | 30 | let mut output = source.to_vec(); 31 | let len = in_place(&mut output, &cfg).expect("failed to minify"); 32 | output.truncate(len); 33 | if let Some(output_dir) = &output_dir { 34 | fs::write(format!("{}/{}", output_dir, input_name), output).unwrap(); 35 | }; 36 | 37 | let start = Instant::now(); 38 | for _ in 0..iterations { 39 | let mut data = source.to_vec(); 40 | let _ = in_place(&mut data, &cfg).expect("failed to minify"); 41 | } 42 | let elapsed = start.elapsed().as_secs_f64(); 43 | 44 | results.push((input_name, len, iterations, elapsed)); 45 | } 46 | 47 | serde_json::to_writer(stdout(), &results).unwrap(); 48 | } 49 | -------------------------------------------------------------------------------- /bench/runners/minify-html/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minify-html-bench" 3 | publish = false 4 | version = "0.0.1" 5 | authors = ["Wilson Lin "] 6 | edition = "2018" 7 | 8 | [dependencies] 9 | minify-html = { path = "../../../minify-html" } 10 | serde = { version = "1.0.104", features = ["derive"] } 11 | serde_json = "1.0.44" 12 | -------------------------------------------------------------------------------- /bench/runners/minify-html/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | cargo build --release 6 | -------------------------------------------------------------------------------- /bench/runners/minify-html/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | ../../../target/release/minify-html-bench 6 | -------------------------------------------------------------------------------- /bench/runners/minify-html/src/main.rs: -------------------------------------------------------------------------------- 1 | use minify_html::minify; 2 | use minify_html::Cfg; 3 | use std::env; 4 | use std::fs; 5 | use std::io::stdout; 6 | use std::time::Instant; 7 | 8 | fn main() { 9 | let iterations = env::var("MHB_ITERATIONS") 10 | .unwrap() 11 | .parse::() 12 | .unwrap(); 13 | let input_dir = env::var("MHB_INPUT_DIR").unwrap(); 14 | let html_only = env::var("MHB_HTML_ONLY") 15 | .ok() 16 | .filter(|v| v == "1") 17 | .is_some(); 18 | let output_dir = env::var("MHB_OUTPUT_DIR").ok(); 19 | 20 | let mut results: Vec<(String, usize, usize, f64)> = Vec::new(); 21 | let mut cfg = Cfg::new(); 22 | cfg.enable_possibly_noncompliant(); 23 | if !html_only { 24 | cfg.minify_css = true; 25 | cfg.minify_js = true; 26 | }; 27 | 28 | for t in fs::read_dir(input_dir).unwrap().map(|d| d.unwrap()) { 29 | let source = fs::read(t.path()).unwrap(); 30 | let input_name = t.file_name().into_string().unwrap(); 31 | 32 | let output = minify(&source, &cfg); 33 | let len = output.len(); 34 | if let Some(output_dir) = &output_dir { 35 | fs::write(format!("{}/{}", output_dir, input_name), output).unwrap(); 36 | }; 37 | 38 | let start = Instant::now(); 39 | for _ in 0..iterations { 40 | let _ = minify(&source, &cfg); 41 | } 42 | let elapsed = start.elapsed().as_secs_f64(); 43 | 44 | results.push((input_name, len, iterations, elapsed)); 45 | } 46 | 47 | serde_json::to_writer(stdout(), &results).unwrap(); 48 | } 49 | -------------------------------------------------------------------------------- /bench/runners/minimize/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | npm i 6 | -------------------------------------------------------------------------------- /bench/runners/minimize/index.js: -------------------------------------------------------------------------------- 1 | const minimize = require("minimize"); 2 | const { htmlOnly, esbuildCss, esbuildJs, run } = require("../common"); 3 | 4 | const jsMime = new Set([ 5 | undefined, 6 | "application/ecmascript", 7 | "application/javascript", 8 | "application/x-ecmascript", 9 | "application/x-javascript", 10 | "text/ecmascript", 11 | "text/javascript", 12 | "text/javascript1.0", 13 | "text/javascript1.1", 14 | "text/javascript1.2", 15 | "text/javascript1.3", 16 | "text/javascript1.4", 17 | "text/javascript1.5", 18 | "text/jscript", 19 | "text/livescript", 20 | "text/x-ecmascript", 21 | "text/x-javascript", 22 | ]); 23 | 24 | const jsCssPlugin = { 25 | id: "esbuild", 26 | element: (node, next) => { 27 | if (node.type === "text" && node.parent) { 28 | if ( 29 | node.parent.type === "script" && 30 | jsMime.has(node.parent.attribs.type) 31 | ) { 32 | node.data = esbuildJs(node.data); 33 | } else if (node.parent.type === "style") { 34 | node.data = esbuildCss(node.data); 35 | } 36 | } 37 | next(); 38 | }, 39 | }; 40 | 41 | const plugins = htmlOnly ? [] : [jsCssPlugin]; 42 | 43 | const minifier = new minimize({ plugins }); 44 | 45 | run((src) => minifier.parse(src.toString())); 46 | -------------------------------------------------------------------------------- /bench/runners/minimize/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "minimize": "2.2.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /bench/runners/minimize/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | node index.js 6 | -------------------------------------------------------------------------------- /bench/runners/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "esbuild": "0.12.19" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /debug/diff/README.md: -------------------------------------------------------------------------------- 1 | [compare](./compare) is a useful script for viewing a character-by-character diff between the minified outputs of minify-html and html-minifier for a specific input. Pass the input's file name as the first argument. 2 | -------------------------------------------------------------------------------- /debug/diff/c14n/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | publish = false 3 | name = "c14n" 4 | version = "0.0.1" 5 | edition = "2018" 6 | 7 | [dependencies] 8 | minify-html = { path = "../../../minify-html" } 9 | -------------------------------------------------------------------------------- /debug/diff/c14n/README.md: -------------------------------------------------------------------------------- 1 | # c14n 2 | 3 | Parse HTML from stdin and write a canonical HTML document to stdout. Useful to preprocess documents for diffing: 4 | 5 | - Sort attributes by name. 6 | - Decode all entities, then re-encode only special characters consistently. 7 | - Make tag and attribute names lowercase. 8 | -------------------------------------------------------------------------------- /debug/diff/c14n/src/main.rs: -------------------------------------------------------------------------------- 1 | use minify_html::canonicalise; 2 | use std::io::stdin; 3 | use std::io::stdout; 4 | use std::io::Read; 5 | 6 | fn main() { 7 | let mut src = Vec::new(); 8 | stdin().read_to_end(&mut src).unwrap(); 9 | canonicalise(&mut stdout(), &src).unwrap(); 10 | } 11 | -------------------------------------------------------------------------------- /debug/diff/canonicalise: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeo pipefail 4 | 5 | pushd "$(dirname "$0")" >/dev/null 6 | 7 | cargo build --manifest-path c14n/Cargo.toml --release 8 | cargo build --manifest-path charlines/Cargo.toml --release 9 | 10 | for f in outputs/*/*; do 11 | out=$(../../target/release/c14n < "$f") 12 | if [[ "$CHARLINES" == "1" ]]; then 13 | out=$(../../target/release/charlines <<< "$out") 14 | fi 15 | cat <<< "$out" > "$f" 16 | done 17 | 18 | popd >/dev/null 19 | -------------------------------------------------------------------------------- /debug/diff/charlines/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | publish = false 3 | name = "charlines" 4 | version = "0.0.1" 5 | edition = "2018" 6 | -------------------------------------------------------------------------------- /debug/diff/charlines/README.md: -------------------------------------------------------------------------------- 1 | # charlines 2 | 3 | Output each character from stdin onto its own stdout line. Useful for subsequence diffing when text does not naturally have a lot of line breaks (e.g. minified HTML). 4 | -------------------------------------------------------------------------------- /debug/diff/charlines/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::stdin; 2 | use std::io::stdout; 3 | use std::io::Read; 4 | use std::io::Write; 5 | 6 | fn main() { 7 | let mut src = Vec::new(); 8 | stdin().read_to_end(&mut src).unwrap(); 9 | let mut out = stdout(); 10 | for c in src { 11 | out.write_all(&[c, b'\n']).unwrap(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /debug/diff/compare: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | pushd "$(dirname "$0")" >/dev/null 6 | 7 | git --no-pager diff --no-index --word-diff=color --word-diff-regex=. "outputs/html-minifier/$1" "outputs/minify-html/$1" | less 8 | 9 | popd >/dev/null 10 | -------------------------------------------------------------------------------- /debug/diff/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuo pipefail 4 | 5 | shopt -s nullglob 6 | 7 | pushd "$(dirname "$0")" >/dev/null 8 | 9 | input_dir="$PWD/../../bench/inputs" 10 | output_dir="$PWD/outputs" 11 | 12 | rm -rf "$output_dir" 13 | mkdir -p "$output_dir" 14 | 15 | pushd "../../bench/runners" >/dev/null 16 | for r in *; do 17 | if [[ ! -d "$r" ]] || [[ "$r" == "node_modules" ]]; then 18 | continue 19 | fi 20 | mkdir -p "$output_dir/$r" 21 | echo "Running $r..." 22 | pushd "$r" >/dev/null 23 | MHB_ITERATIONS=1 MHB_INPUT_DIR="$input_dir" MHB_OUTPUT_DIR="$output_dir/$r" RUST_BACKTRACE=1 ./run >/dev/null 24 | popd >/dev/null 25 | done 26 | popd >/dev/null 27 | 28 | echo "All done!" 29 | 30 | popd >/dev/null 31 | -------------------------------------------------------------------------------- /debug/prof/README.md: -------------------------------------------------------------------------------- 1 | Profiling minify-html can be done on Linux by using [profile.sh](./profile.sh), which uses `perf`. The generated report can be used using `perf report`. 2 | -------------------------------------------------------------------------------- /debug/prof/profile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | shopt -s nullglob 5 | 6 | for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do 7 | echo performance | sudo dd status=none of="$i" 8 | done 9 | 10 | rm -f perf.data 11 | sudo perf record -g nice -n -20 taskset -c 1 target/release/minify-html-bench --tests tests --iterations 512 12 | sudo chown "$USER:$USER" perf.data 13 | -------------------------------------------------------------------------------- /format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuxo pipefail 4 | 5 | pushd "$(dirname "$0")" >/dev/null 6 | 7 | npx prettier@2.3.2 -w \ 8 | 'bench/*.{js,json}' \ 9 | 'bench/runners/*.{js,json}' \ 10 | 'bench/runners/*/*.{js,json}' \ 11 | 'minify-html-nodejs/*.{js,json,ts}' \ 12 | 'version' 13 | 14 | cargo +nightly fmt 15 | -------------------------------------------------------------------------------- /icon/cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/icon/cli.png -------------------------------------------------------------------------------- /icon/deno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/icon/deno.png -------------------------------------------------------------------------------- /icon/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/icon/java.png -------------------------------------------------------------------------------- /icon/nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/icon/nodejs.png -------------------------------------------------------------------------------- /icon/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/icon/python.png -------------------------------------------------------------------------------- /icon/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/icon/ruby.png -------------------------------------------------------------------------------- /icon/rust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/icon/rust.png -------------------------------------------------------------------------------- /icon/wasm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsonzlin/minify-html/2301223773dadce30a33b4c407d8b2524adeb5e2/icon/wasm.png -------------------------------------------------------------------------------- /minhtml/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minhtml" 3 | description = "[CLI] Extremely fast and smart HTML + JS + CSS minifier" 4 | version = "0.16.4" 5 | authors = ["Wilson Lin "] 6 | edition = "2018" 7 | license = "MIT" 8 | homepage = "https://github.com/wilsonzlin/minify-html" 9 | readme = "README.md" 10 | keywords = ["html", "compress", "minifier", "js", "css"] 11 | categories = ["compression", "command-line-utilities", "development-tools::build-utils", "web-programming"] 12 | repository = "https://github.com/wilsonzlin/minify-html.git" 13 | 14 | [dependencies] 15 | minify-html = { version = "0.16.4", path = "../minify-html" } 16 | rayon = "1.5" 17 | structopt = "0.3" 18 | -------------------------------------------------------------------------------- /minhtml/README.md: -------------------------------------------------------------------------------- 1 | # minhtml 2 | 3 | CLI for [minify-html](https://github.com/wilsonzlin/minify-html). 4 | 5 | ## Usage 6 | 7 | ``` 8 | minhtml [FLAGS] [OPTIONS] [inputs]... 9 | ``` 10 | 11 | - **-o, --output \**: Output destination; omit for stdout 12 | - **\...**: Files to minify; omit for stdin. If more than one is provided, they will be parallel minified in place, and --output must be omitted 13 | 14 | ### Flags 15 | 16 | |Arg|Description| 17 | |---|---| 18 | |--do-not-minify-doctype|Do not minify DOCTYPEs. Minified DOCTYPEs may not be spec compliant| 19 | |--ensure-spec-compliant-unquoted-attribute-values|Ensure all unquoted attribute values in the output do not contain any characters prohibited by the WHATWG specification| 20 | |-h, --help|Prints help information| 21 | |--keep-closing-tags|Do not omit closing tags when possible| 22 | |--keep-comments|Keep all comments| 23 | |--keep-ssi-comments|Keep SSI comments| 24 | |--keep-html-and-head-opening-tags|Do not omit `` and `` opening tags when they don't have attributes| 25 | |--keep-spaces-between-attributes|Keep spaces between attributes when possible to conform to HTML standards| 26 | |--minify-css|Minify CSS in `