Search by

yahyaerturan / minifier

yahyaerturan

HTML/JS/CSS minifier with template-awareness for PHP 8.4.

Package info

github.com/yahyaerturan/minifier

pkg:composer/yahyaerturan/minifier

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2025-11-05 15:22 UTC

This package is auto-updated.

Last update: 2026-10-13 17:37:52 UTC


README

A conservative, production-ready minifier for HTML with embedded CSS/JS and JSON. It’s template-aware (Mustache/Handlebars/Twig-style tokens), safe around </script> landmines, and intentionally avoids risky AST rewrites.

  • Zero dependencies, PHP 8.4+
  • Template fences: {{ }}, {{{ }}}, {% %}, {# #}, [[ ]] are protected and restored
  • Robust <script> handling: finds the real closing tag while skipping strings, comments, template literals, and CDATA
  • Sentinel escape: </script> → <\/script> inside JS/JSON to prevent HTML parser bail-outs
  • Conservative JS/CSS: strips comments, collapses whitespace safely; preserves /*! … */ and comments with @license/@preserve verbatim
  • JSON scripts (application/ld+json): minified via json_decode/json_encode with a safe fallback for not-strictly-valid inputs
  • Inline style="" compacted using the same CSS minifier
  • Whitespace collapsed outside pre/code/textarea/script/style without stripping inter-tag spaces that can be visually significant (no global >< gluing).

This library is for correctness first. If you want heavy optimization (renaming, dead-code elimination), use a separate build step and keep this as the final safety pass.

Installation

composer require yahyaerturan/minifier

Requires: PHP 8.4+

Quick start

use YahyaErturan\Minifier\HtmlMinifier;
use YahyaErturan\Minifier\Options;

$min = new HtmlMinifier(new Options()); // safe defaults
$compact = $min->minify($html);

What you get by default:

  • HTML comments removed except conditional <!--[if …]> and bang <!--! … -->
  • Inline <style> and style="" compacted
  • <script> bodies minified for classic JS + modules, </script> escaped
  • application/ld+json compacted structurally
  • Templating tokens fenced and restored

Ignore controls (escape hatches)

Sometimes a third-party snippet or unusual markup breaks when minified. Use these “do not touch” controls.

HTML directives (comments)

  • Region pair:

    • <!-- minify:off --> … <!-- minify:on -->
    • The enclosed region is bypassed entirely (no minification, no sentinel escaping). The control comments themselves are removed.
  • One-shot:

    • <!-- minify:ignore-next --> <script>…</script>
    • <!-- minify:ignore-next --> <style>…</style>
    • The very next <script>/<style> is output verbatim; control comment removed.

Per-tag attributes

  • On <script> or <style>:
    • data-minify="off" → do not minify the tag body. For <script>, the sentinel escape still applies for safety.
    • data-minify="raw" → output the tag verbatim (no minify, no sentinel escape). Sharp tool; use only when you truly need raw.
  • The data-minify attribute is stripped from the output (build hint does not leak).
  • Unquoted attributes are supported: type=module, data-minify=off|raw work the same as quoted.

Options

final class Options
{
    public function __construct(
        // HTML
        public bool $collapseHtmlWhitespace = true,
        public bool $removeHtmlComments = true,
        public bool $preserveBangHtmlComments = true,
        public bool $preserveConditionalComments = true,

        // Inline assets
        public bool $minifyInlineCss = true,
        public bool $minifyInlineJs = true,

        // JS
        public bool $removeJsComments = true,
        public bool $preserveImportantJsComments = true, // /*! … */ or @license/@preserve kept verbatim
        public bool $retainScriptSentinel = true,        // keep /* <\/script> */ when removed comments had </script>

        // CSS
        public bool $removeCssComments = true,
        public bool $preserveImportantCssComments = true, // /*! … */

        // JSON scripts
        public bool $minifyJsonScripts = true,

        // Template tokens
        public array $templateDelimiters = [
            ['{{{','}}}'],
            ['{{','}}'],
            ['{%','%}'],
            ['{#','#}'],
            ['[[',']]'],
        ],

        // Ignore directives
        public bool   $enableIgnoreDirectives = true,
        public string $ignoreOpenComment      = 'minify:off',
        public string $ignoreCloseComment     = 'minify:on',
        public string $ignoreNextComment      = 'minify:ignore-next',
        public string $ignoreAttrName         = 'data-minify',
    ) {}
}

Notes:

  • retainScriptSentinel: when a removed JS comment contained </script> (pre-escaped), the minifier emits a tiny placeholder /* <\/script> */. Why: guarantees safety if another tool concatenates content later. Trade-off: a few bytes in rare cases.
  • templateDelimiters: longest opener wins; add your own pairs if you use a different templating engine.
  • Ignore controls: off keeps <\/script> safety for <script>; raw is truly raw (no safety).

Script handling policy

Goal: Only minify when it’s definitely JavaScript; treat known template types as opaque; always keep </script> safe.

Script type / condition Action
<script> with no type Minify JS + escape </script>
type="module" (quoted or unquoted) Minify JS + escape </script>
type is a recognized JS MIME: text/javascript, application/javascript, text/ecmascript, application/ecmascript, x-javascript Minify JS + escape </script>
type="application/ld+json" or application/json JSON structural minify + escape </script>
type is a template: text/template, text/x-template, text/handlebars, text/ng-template, application/x-tmpl Skip minification (opaque)
data-minify="off" Do not minify; still escape </script>
data-minify="raw" Output verbatim; no escaping
Any other/unknown type Do not minify; do escape </script> (defensive default)

Unquoted attribute support: HTML allows tokens without quotes; this minifier recognizes type=module and data-minify=off|raw even when unquoted.

Why conservative?

Pros: avoids broken pages, keeps template engines happy, plays well with CSP and weird embeds.
Cons: not byte-optimal; doesn’t rewrite semantics or identifiers.
Bottom line: use aggressive tools in CI if you need them; run this as your last line of defense in prod.

Framework integration (sketches)

Laravel middleware:

class MinifyHtml
{
    public function handle($request, \Closure $next)
    {
        $response = $next($request);
        if (str_contains($response->headers->get('Content-Type', ''), 'text/html')) {
            $min = new \YahyaErturan\Minifier\HtmlMinifier(new \YahyaErturan\Minifier\Options());
            $response->setContent($min->minify($response->getContent()));
        }
        return $response;
    }
}

Symfony kernel response listener:

public function onKernelResponse(ResponseEvent $event): void
{
    $res = $event->getResponse();
    if (str_contains($res->headers->get('Content-Type', ''), 'text/html')) {
        $min = new \YahyaErturan\Minifier\HtmlMinifier(new \YahyaErturan\Minifier\Options());
        $res->setContent($min->minify($res->getContent()));
    }
}

Safety & security

  • CSP: if you hash inline scripts/styles ('sha256-…'), minify before computing hashes.
  • XSS: the minifier never unescapes entities or rewrites URLs; it only strips comments and collapses whitespace. It also neutralizes </script> sequences with <\/script> to prevent HTML parser bail-out (unless you request raw).
  • Templates: tokens are fenced and restored verbatim. If you use exotic delimiters, add them to templateDelimiters.
  • XSS/HTML parser: </script> sequences inside JS/JSON are always neutralized as <\/script>.
  • Whitespace: The minifier compresses text nodes but intentionally keeps necessary spaces between HTML tags to avoid layout/text regressions.

Tests & QA

composer install
composer test

Recommended extras:

  • Static analysis (PHPStan/Psalm)
  • Mutation testing (Infection)
  • Canary & monitor

Known limits (by design)

  • No AST-level JS/CSS transforms (no mangling, dead-code removal, or re-ordering).
  • No DOM reflow: the HTML pass doesn’t restructure tags or attributes.
  • SVG/MathML treated as regular HTML except inside <style> or attributes—safer default.

Versioning

Stable series: 1.0.x (patch releases are backwards-compatible).

License

MIT © Yahya Erturan