No 3rd party dependencies. All dependencies and devDependencies, checked recursively, are Codsen packages.
Permalink to InstallationInstallation
Permalink to Quick TakeQuick Take
Permalink to ExamplesExamples
- Configure CSS comment removal
- Insert line breaks before selected tokens
- Treat a custom element as inline
- Choose which HTML comments to remove
- Wrap minified HTML at a line-length limit
- Preserve CSS token boundaries when removing comments
- Preserve whitespace that terminates CSS escapes
- Preserve encoded inline CSS
- Observe progress in a custom interval
- Remove line indentations
- Remove HTML line breaks
- Inspect result metadata and edit ranges
Purpose
html-crush minifies mixed HTML and CSS, with a focus on email templates. It
scans the input without parsing it into an abstract syntax tree, so it can work
with incomplete markup and templates that contain Nunjucks, JSP, ESP, CDATA,
or other embedded languages.
Email HTML needs controls that general web-page minifiers do not always provide. For example, you can preserve Outlook conditional comments and place line breaks before selected tokens to keep generated lines within an email transport limit.
The scanner preserves ordinary quoted attribute values, CSS strings and URL data, and the
contents of script, pre, code, and textarea elements. HTML element names
are matched case-insensitively.
CSS escapes and whitespace
Whitespace can be part of a CSS escape or separate two selectors. crush() preserves escaped tokens and their following separators in style elements and inline styles. For example, the first space after \31 terminates the escape for 1; the second space creates a descendant selector:
import { crush } from "html-crush";
const html = String.raw`<style>.\31 a{color:red}</style>`;
console.log(crush(html).result === html);
// => true
This protection applies to indentation removal, line-break removal, wrapping, and final whitespace cleanup. An escaped space, as in font-family:a\ b, stays inside its CSS token. The preferred line-length limit does not force a break inside a protected token or separator.
CSS strings, URLs, and HTML character references
Quotes escaped inside CSS identifiers do not start strings. Comment markers inside CSS strings or URL tokens stay literal, including unquoted URLs and escaped spellings of url.
Inline styles use HTML character-reference decoding once before CSS token recognition. For example, " can create a CSS string delimiter, and \ can create a CSS escape. Whitespace and wrapping changes preserve an inline attribute conservatively when its raw source contains character references:
import { crush } from "html-crush";
const html = '<b style="content:"a b";font-family:\31 a">x</b>';
const result = crush(html);
console.log(result.result === html);
// => true
console.log(result.applicableOpts.removeCSSComments);
// => false
Real comments are identified in the decoded CSS, and their original HTML source spans are used for edits. applicableOpts.removeCSSComments reports whether safe comment removal could change the input, independently of whether removal is enabled. Style-element text is CSS raw text, so HTML character references there are not decoded.
HTML boundaries take precedence over unfinished CSS. A literal closing style tag ends a style element, and the literal HTML quote ends a quoted style attribute even if its CSS contains an unfinished string or comment. Invalid CSS string newlines remain boundaries during whitespace cleanup.
Comments and CSS token boundaries
Removing a CSS comment must preserve the tokens on both sides. For example, 1/* note */px contains separate tokens; deleting the comment would create the dimension 1px. Adding a space can also change a compound selector into a descendant selector.
With removeCSSComments: true, comments at ordinary block and declaration boundaries are removed. When deletion could join tokens, a minimal /**/ remains. Existing separators beside that boundary are protected from whitespace cleanup and wrapping:
import { crush } from "html-crush";
const html = String.raw`<style>.a\31/* explanation */ a{color:red}</style>`;
console.log(crush(html).result);
// => <style>.a\31/**/ a{color:red}</style>
Here, the selector still matches a descendant a inside class a1. The same boundary protection applies to decoded inline CSS and to retained comments when removal is disabled. applicableOpts.removeCSSComments is false for an already minimal required /**/ separator. A longer comment, or an encoded raw comment that can become /**/, still makes removal applicable.
API — crush()
The main function crush() is imported like this:
Pass the HTML source as a string and, optionally, an options object or null:
function crush(str: string, opts?: InputOpts | null): Res;
InputOpts has the following shape. Every property is optional:
interface InputOpts {
lineLengthLimit?: number;
removeIndentations?: boolean;
removeLineBreaks?: boolean;
removeHTMLComments?: boolean | 0 | 1 | 2;
removeCSSComments?: boolean;
reportProgressFunc?: null | false | 0 | ((percentage: number) => void);
reportProgressFuncFrom?: number;
reportProgressFuncTo?: number;
breakToTheLeftOf?: string[] | null | false;
mindTheInlineTags?: string[];
}
| Key | Input type | Default | Description |
|---|---|---|---|
lineLengthLimitDefault: 500 | |||
lineLengthLimit | number | 500 | Sets the preferred maximum line length when removeLineBreaks is enabled. |
removeIndentationsDefault: true | |||
removeIndentations | boolean | true | Removes indentation while retaining line breaks. Enabling removeLineBreaks also enables this behavior. |
removeLineBreaksDefault: false | |||
removeLineBreaks | boolean | false | Removes existing line breaks, subject to lineLengthLimit and breakToTheLeftOf. |
removeHTMLCommentsDefault: false | |||
removeHTMLComments | boolean | 0 | 1 | 2 | false | false or 0 keeps comments. true or 1 removes ordinary comments but preserves Outlook conditionals. 2 removes all recognized HTML comments, including conditionals. |
removeCSSCommentsDefault: true | |||
removeCSSComments | boolean | true | Removes recognized CSS comments in style elements and style attributes. Comment-like text inside CSS strings and URL tokens is preserved. |
reportProgressFuncDefault: null | |||
reportProgressFunc | Function or null, false, or 0 | null | Receives integer progress values for sufficiently large inputs. See Progress reporting. |
reportProgressFuncFromDefault: 0 | |||
reportProgressFuncFrom | number | 0 | Sets the beginning of the reported progress interval. |
reportProgressFuncToDefault: 100 | |||
reportProgressFuncTo | number | 100 | Sets the end of the reported progress interval. |
breakToTheLeftOfDefault: See below | |||
breakToTheLeftOf | string[] | null | false | See below | Starts a new line before a matching token when removeLineBreaks is enabled. Pass false, null, or an empty array to disable the defaults. |
mindTheInlineTagsDefault: See below | |||
mindTheInlineTags | string[] | See below | Lists inline element names whose surrounding whitespace needs extra protection. |
Here are all defaults in one place for copying:
The function returns a plain Res object:
type Range =
| [from: number, to: number]
| [from: number, to: number, whatToInsert: string | null | undefined];
interface Res {
log: {
timeTakenInMilliseconds: number;
originalLength: number;
cleanedLength: number;
bytesSaved: number;
percentageReducedOfOriginal: number;
originalLengthInCodeUnits: number;
cleanedLengthInCodeUnits: number;
codeUnitsSaved: number;
percentageReducedOfOriginalInCodeUnits: number;
originalLengthInUtf8Bytes: number;
cleanedLengthInUtf8Bytes: number;
utf8BytesSaved: number;
percentageReducedOfOriginalInUtf8Bytes: number;
};
applicableOpts: {
removeHTMLComments: boolean;
removeCSSComments: boolean;
};
ranges: Range[] | null;
result: string;
}
| Key | Description |
|---|---|
result | |
result | The minified HTML string. |
ranges | |
ranges | The edits in ranges notation, or null when no edit is needed. |
applicableOpts | |
applicableOpts | Reports whether the input contains removable HTML or CSS comments, independently of the current removal settings. |
log | |
log | Contains elapsed time and size statistics. |
The result contains only plain objects, arrays, strings, numbers, booleans, and
null. It can be serialized as JSON or sent through postMessage().
Size and timing fields
JavaScript string lengths count UTF-16 code units, not bytes. The explicit fields distinguish those lengths from the UTF-8 size that an encoded email would occupy. Percentages are rounded to the nearest integer.
log field | Meaning |
|---|---|
timeTakenInMilliseconds | |
timeTakenInMilliseconds | Best-effort elapsed time. Do not use it for exact comparisons. |
originalLength | |
originalLength | Legacy input length in UTF-16 code units. |
cleanedLength | |
cleanedLength | Legacy output length in UTF-16 code units. |
bytesSaved | |
bytesSaved | Legacy name for UTF-16 code units saved. This field does not contain a byte count. |
percentageReducedOfOriginal | |
percentageReducedOfOriginal | Legacy reduction percentage calculated from UTF-16 code units. |
originalLengthInCodeUnits | |
originalLengthInCodeUnits | Input length in UTF-16 code units. |
cleanedLengthInCodeUnits | |
cleanedLengthInCodeUnits | Output length in UTF-16 code units. |
codeUnitsSaved | |
codeUnitsSaved | Number of UTF-16 code units removed. |
percentageReducedOfOriginalInCodeUnits | |
percentageReducedOfOriginalInCodeUnits | Reduction percentage calculated from UTF-16 code units. |
originalLengthInUtf8Bytes | |
originalLengthInUtf8Bytes | Input size in UTF-8 bytes. |
cleanedLengthInUtf8Bytes | |
cleanedLengthInUtf8Bytes | Output size in UTF-8 bytes. |
utf8BytesSaved | |
utf8BytesSaved | Number of UTF-8 bytes removed. |
percentageReducedOfOriginalInUtf8Bytes | |
percentageReducedOfOriginalInUtf8Bytes | Reduction percentage calculated from UTF-8 bytes. |
For compatibility, the four legacy size fields keep their existing UTF-16 code-unit meaning. Use the explicit fields in new integrations.
Progress reporting
Use reportProgressFunc when crush() runs in a Web Worker or another UI that
needs completion feedback. Inputs of 1,000 code units or fewer do not call the
callback. Inputs from 1,001 through 1,999 code units report a midpoint and
completion. Longer inputs report incremental values.
Every reported value is an integer within the interval configured by
reportProgressFuncFrom and reportProgressFuncTo. Both changed and unchanged
inputs finish at the configured ending value. The callback is observational:
it does not change the result, ranges, or deterministic statistics.
import { crush } from "html-crush";
const res = crush(source, {
removeLineBreaks: true,
reportProgressFunc: (percentage) => {
postMessage({ type: "progress", percentage });
},
});
postMessage({ type: "result", res });
API — defaults
You can import defaults:
It's a plain object:
The main function calculates the options to be used by merging the options you passed with these defaults.
API — version
You can import version:
API — types
This package is written in TypeScript and exports these public types:
| Type | Description |
|---|---|
InputOptsType: InputOpts | |
InputOpts | The optional input accepted by crush(), including the supported disabling sentinels. |
OptsType: Opts | |
Opts | The fully resolved internal option shape after defaults and input normalization. |
ResType: Res | |
Res | The minified result, edit ranges, applicability report, and completion statistics. |
import type { InputOpts, Opts, Res } from "html-crush";