Skip to content

Rewrite URL mappings in streamed CSS file chunks - #314

Open
adamziel wants to merge 41 commits into
trunkfrom
codex/stream-resumable-css
Open

Rewrite URL mappings in streamed CSS file chunks#314
adamziel wants to merge 41 commits into
trunkfrom
codex/stream-resumable-css

Conversation

@adamziel

@adamziel adamziel commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Rewrites source-site URLs as CSS file chunks arrive. A migration can read a file in parts, write each result, and resume after the PHP process stops.

URL changes

With https://old.example mapped to https://new.example/local:

Input CSS Output CSS
url("https://old.example/photo.png?v=2#crop") url("https://new.example/local/photo.png?v=2#crop")
@import "https://old.example/theme.css"; @import "https://new.example/local/theme.css";
image-set("//old.example/photo.png" 1x) image-set("//new.example/local/photo.png" 1x)
url(https:\/\/old.example\/photo.png) url(https://new.example/local\/photo.png)
a{content:"https://old.example/photo.png"} Unchanged: this is displayed text.
/* url(https://old.example/photo.png) */ Unchanged: this is a comment.
url(https://old.example.org/photo.png) Unchanged: this is a different host.

Only the matched URL base changes. Quotes, parentheses, spaces, and the remaining URL bytes keep their original form. The escaped slash before photo.png in the fourth row stays escaped.

The longest matching source base wins. Scheme and host matching ignores letter case. Path matching uses letter case and requires /, ?, #, or the URL end after the base. For example, a mapping for https://old.example/blog matches /blog/photo.png, but not /blogger/photo.png or /Blog/photo.png.

Split input and resume

This example splits a URL between two calls. It also recreates the processor from JSON. The saved parser state is called a cursor. It includes the unfinished URL.

use WordPress\DataLiberation\URL\CSSURLProcessor;

$mapping = array( 'https://old.example' => 'https://new.example/local' );
$processor = CSSURLProcessor::create_for_streaming( $mapping );

foreach ( $processor->rewrite_chunk( 'a{background:url(https://old.exa', false ) as $chunk ) {
	echo $chunk;
}
$saved_cursor = json_encode( $processor->get_reentrancy_cursor() );
unset( $processor );

$processor = CSSURLProcessor::create_for_streaming(
	$mapping,
	json_decode( $saved_cursor, true )
);
foreach ( $processor->rewrite_chunk( 'mple/photo.png)}', true ) as $chunk ) {
	echo $chunk;
}

Combined output:

a{background:url(https://new.example/local/photo.png)}

For real files, write and flush all output from a call before saving the cursor and both file byte offsets together. On resume, seek the source to its saved offset. Remove output bytes after the saved output offset, then append there. The source offset includes all supplied bytes: the cursor already holds the unfinished part. The file-rewrite test caller shows these steps.

Errors and limits

Resume requires the same URL mappings. For example, restoring the cursor above with https://old.example mapped to https://other.example throws:

Cannot resume CSS rewriting with different URL mappings. Start a new stylesheet rewrite.

Saving a cursor while output remains unread also throws. Source and target bases must be HTTP(S) URLs without credentials, query strings, or fragments. Set is_last to true only at the actual end of the CSS file.

Each output chunk is at most 64 KiB. This does not limit total memory use. An unfinished CSS unit, such as a comment or URL, stays in memory and in the cursor. It is parsed again when more input arrives. A large unfinished unit therefore increases memory use, saved state size, and parsing work. More than 128 open, nested image-set() functions causes an error.

Testing

The stream tests check each possible byte split in their CSS examples, JSON resume, escaped URLs, output expansion, changed mappings, and attempts to save state before reading all output.

Separate PHP processes rewrite real files. One case runs to completion. Two cases stop just before or just after saving state, then resume in a new process. All three must produce the expected output. The tests also check that a nesting-limit error occurs on both the first run and resume. The source file stays unchanged, and the last saved state remains available.

Dependencies

The prerequisite PRs #315, #316, and #317 are merged. This PR adds URL rewriting to the streaming CSS parser without changing CSSProcessor. WordPress/reprint#764 needs a published package release with this change before it can use the API.

@adamziel adamziel changed the title Stream and resume CSS URL rewriting Rewrite URL mappings in streamed CSS file chunks Sep 8, 2026
@adamziel
adamziel changed the base branch from trunk to codex/css-token-stream September 8, 2026 08:48
adamziel added a commit that referenced this pull request Sep 9, 2026
Lets a CSS token span input chunks and continue in a new PHP process
without restarting the file.

Review order: #315 (merged) → #316#317#314.

The processor keeps unfinished input and parses it again after another
read. Whole strings and streamed input use the same `next_token()`,
getters, and value setter. `flush_processed_css()` returns edited,
completed input and releases its source bytes. The cursor contains the
remaining bytes and whether more input is expected; the caller saves it
with the source and output offsets after writing the output.

This is layer 3 of the CSS stack, based on #316. It adds input,
flushing, and the token cursor together. It contains no URL mappings or
`rewrite_chunk()`; those remain in #314. ASCII spans are scanned
together so reparsing long identifiers and URLs does not walk every byte
in PHP.

A large token increases memory use, cursor size, and repeated parsing
work. Token size is not capped. The README spells out that cost and
links the real-file caller.

## Testing

The streamed tokens match the whole-string corpus, including one-byte
reads, escapes, and invalid UTF-8. Separate PHP processes edit a
multi-chunk file, stop before or after a saved checkpoint, and resume
with the unfinished token. The written file must match whole-string
edits without missing or duplicated bytes. The full DataLiberation run
completed with 2,618 tests and 16 skips. PHPCS passes.
Base automatically changed from codex/css-token-stream to trunk September 9, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant