You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In a self-extracting archive, a stylesheet is stored once per content: several <link> elements that resolve to the same CSS share one file whatever URL each came from, and an @import chain is merged from the leaves up, so two stylesheets differing only in the file they import merge once their imports have. Measured over 24 real pages that is 736 stylesheet files down to 263, and 3.2 MB less CSS for the browser to parse when the saved page is opened. The key is the content and not the URL, so copies the unused-styles pass left different from each other stay separate files
In a self-extracting archive, the images SingleFile generates itself are stored as files instead of being inlined as data URIs: the snapshot it takes of a video frame for a poster, the icon it puts beside a blocked video, and the bitmap of a <canvas>. Elements that produce the same bytes share one file. A self-contained page is unchanged, since it has nowhere to put a file
The "remove unused styles" pass is faster: the css-tree balance buffer is cleared only where it was used, the revert-layer scan is cached per rule and per declaration, a selector is copied by cloning it rather than re-parsing it, and a nested selector is parsed once instead of twice
Fixes
A standalone @starting-style block was minified as an unconditional group, so its declarations joined the same context as the normal rules, won the order tie-break and pruned the normal declaration. The saved element stayed in its before-transition state
Cascade layers were ordered wrongly in three ways: a sublayer outranked its parent's own declarations, where CSS Cascade 5 puts those in an implicit last sublayer, so @layer a { p { color: red } @layer b { p { color: blue } } } rendered red live and blue saved; every anonymous @layer {} block shared one identity, so two of them were treated as a single layer; and the important twin of each case inverted the same way
Several shapes of revert-layer escaped the protection that keeps what it rolls back to: rolling back to a shorthand declared in the layer below, all: revert-layer, var(--missing, revert-layer), a revert-layer written on a state-dependent rule such as p:hover, and one whose stripped selector matches nothing in the page
@scope roots were resolved wrongly in several cases: a prelude-less @scope nested inside another took the enclosing scope's roots instead of the parent element of the stylesheet's owner node; a scoped selector's match and its scope eligibility could be counted from two different roots; and proximity ignored the limits of the root it was counted to
A nested selector substituted one parent alternative at a time into every &, so .a, .b { & + & { color: red } } was only ever tried as .a + .a and .b + .b, never as the .a + .b the browser matches, and the rule was removed from the saved page
CSS identifiers were compared by their spelling, so an escape or a capital defeated the protections: \61 was not recognised as the layer name a, and the same held for pseudo-class and at-rule names
A vendor-prefixed value was dropped as invalid from every real capture, because only the function's name was tested for support rather than the whole value: background-image: -webkit-linear-gradient(red, blue) disappeared
The assumption that a sanitized selector matches a superset of the real rule did not hold inside :not(), :has() or the of argument of :nth-child(), which the sanitizer does not descend into
An @import url(x.css) layer; naming an anonymous layer with the bare keyword was inlined unlayered, so the imported rules beat every layer they were meant to sit under. An inlined import also lost its supports() condition, and now keeps the layer and the conditions it was written under
An iframe whose src is replaced by srcdoc could be hidden by the page's own CSS: web.dev ships iframe:not([src]) { display: none }, so the saved page computed display: none for a frame the live page showed. An empty src is kept on the element
An archive unzipped and opened from disk lost every stylesheet whose <link> carries crossorigin, because a browser refuses a CORS-mode request for a sibling file:// resource. On a capture of a GitHub page that was all 29 of its 29 links. The attribute is dropped when the URL is rewritten into the archive
Saving a page that SingleFile had already saved changed which elements the structural pseudo-classes match, because the invalid-nesting repair produced a shape a parser does not reproduce
Dependencies
css-tree updated to 3.2.1, which fixes an unquoted url() whose last character is an escaped space