Skip to content

Commit d0c10e8

Browse files
committed
Use a SCS algorithm to add new style sheets
1 parent b5be184 commit d0c10e8

File tree

2 files changed

+153
-61
lines changed

2 files changed

+153
-61
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export function shortestCommonSupersequence< E = unknown >(
2+
X: E[],
3+
Y: E[],
4+
isEqual = ( a: E, b: E ) => a === b
5+
) {
6+
const m = X.length;
7+
const n = Y.length;
8+
9+
// Create a 2D dp table where dp[i][j] is the SCS for X[0..i-1] and Y[0..j-1].
10+
const dp: E[][][] = Array.from( { length: m + 1 }, () =>
11+
Array( n + 1 ).fill( null )
12+
);
13+
14+
// Base cases: one of the sequences is empty.
15+
for ( let i = 0; i <= m; i++ ) {
16+
dp[ i ][ 0 ] = X.slice( 0, i );
17+
}
18+
for ( let j = 0; j <= n; j++ ) {
19+
dp[ 0 ][ j ] = Y.slice( 0, j );
20+
}
21+
22+
// Fill in the dp table.
23+
for ( let i = 1; i <= m; i++ ) {
24+
for ( let j = 1; j <= n; j++ ) {
25+
if ( isEqual( X[ i - 1 ], Y[ j - 1 ] ) ) {
26+
// When X[i-1] equals Y[j-1], use the reference from X.
27+
dp[ i ][ j ] = dp[ i - 1 ][ j - 1 ].concat( X[ i - 1 ] );
28+
} else {
29+
// Choose the shorter option between appending X[i-1] or Y[j-1].
30+
const option1 = dp[ i - 1 ][ j ].concat( X[ i - 1 ] );
31+
const option2 = dp[ i ][ j - 1 ].concat( Y[ j - 1 ] );
32+
dp[ i ][ j ] =
33+
option1.length <= option2.length ? option1 : option2;
34+
}
35+
}
36+
}
37+
38+
return dp[ m ][ n ];
39+
}

packages/interactivity-router/src/assets/styles.ts

+114-61
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,128 @@
1+
/**
2+
* Internal dependencies
3+
*/
4+
import { shortestCommonSupersequence } from './scs';
5+
16
export type StyleElement = HTMLLinkElement | HTMLStyleElement;
27

8+
const isStyleEqual = ( a: StyleElement, b: StyleElement ): boolean => {
9+
if ( a === b ) {
10+
return true;
11+
}
12+
13+
const [ normalizedA, normalizedB ] = [ a, b ].map( ( element ) => {
14+
if ( element.getAttribute( 'media' ) === 'preload' ) {
15+
element = element.cloneNode( true ) as StyleElement;
16+
const { originalMedia } = element.dataset;
17+
if ( originalMedia ) {
18+
element.setAttribute( 'media', originalMedia );
19+
element.removeAttribute( 'data-original-media' );
20+
} else {
21+
element.removeAttribute( 'media' );
22+
}
23+
}
24+
return element;
25+
} );
26+
27+
const result = normalizedA.isEqualNode( normalizedB );
28+
29+
return result;
30+
};
31+
32+
export function updateStylesWithSCS(
33+
X: StyleElement[],
34+
Y: StyleElement[],
35+
parent: Element = window.document.head
36+
) {
37+
if ( X.length === 0 ) {
38+
return Y.map( ( element ) => {
39+
parent.appendChild( element );
40+
return prepareStyleElement( element );
41+
} );
42+
}
43+
44+
const scs = shortestCommonSupersequence( X, Y, isStyleEqual );
45+
const xLength = X.length;
46+
const yLength = Y.length;
47+
const promises = [];
48+
let last = X[ xLength - 1 ];
49+
let xIndex = 0;
50+
let yIndex = 0;
51+
52+
for ( const element of scs ) {
53+
if ( xIndex < xLength && isStyleEqual( X[ xIndex ], element ) ) {
54+
if ( yIndex < yLength && isStyleEqual( Y[ yIndex ], element ) ) {
55+
promises.push( Promise.resolve( X[ xIndex ] ) );
56+
yIndex++;
57+
}
58+
xIndex++;
59+
} else {
60+
const clone = Y[ yIndex ].cloneNode( true ) as StyleElement;
61+
promises.push( prepareStyleElement( clone ) );
62+
if ( xIndex < xLength ) {
63+
X[ xIndex ].before( clone );
64+
yIndex++;
65+
} else {
66+
last.after( clone );
67+
last = clone;
68+
}
69+
}
70+
}
71+
72+
return promises;
73+
}
74+
75+
const prepareStyleElement = (
76+
element: StyleElement
77+
): Promise< StyleElement > => {
78+
if ( element.media ) {
79+
element.dataset.originalMedia = element.media;
80+
}
81+
82+
element.media = 'preload';
83+
84+
if ( element instanceof HTMLStyleElement ) {
85+
return Promise.resolve( element );
86+
}
87+
88+
const loadPromise = new Promise< HTMLLinkElement >( ( resolve, reject ) => {
89+
element.addEventListener( 'load', () => resolve( element ) );
90+
element.addEventListener( 'error', ( event ) => {
91+
const { href } = event.target as HTMLLinkElement;
92+
reject(
93+
Error(
94+
`The style sheet with the following URL failed to load. ${ href }`
95+
)
96+
);
97+
} );
98+
} );
99+
100+
return loadPromise;
101+
};
102+
3103
const styleSheetCache = new Map< string, Promise< StyleElement >[] >();
4104

5105
export const prepareStyles = (
6106
doc: Document,
7107
url: string = ( doc.location || window.location ).href
8108
): Promise< StyleElement >[] => {
9109
if ( ! styleSheetCache.has( url ) ) {
10-
if ( doc !== window.document ) {
11-
window.document.head.appendChild(
12-
window.document.createComment(
13-
`@wordpress/interactivity-router: prefetched styles for ${ url }`
14-
)
15-
);
16-
}
17-
styleSheetCache.set(
18-
url,
19-
[ ...doc.querySelectorAll( 'style,link[rel=stylesheet]' ) ].map(
20-
( element: StyleElement ) => {
21-
if ( doc === window.document ) {
22-
return Promise.resolve( element );
23-
}
24-
25-
const cloned = element.cloneNode( true ) as
26-
| HTMLStyleElement
27-
| HTMLLinkElement;
28-
29-
if ( cloned.media ) {
30-
cloned.dataset.originalMedia = cloned.media;
31-
}
32-
cloned.media = 'preload';
33-
34-
if ( cloned instanceof HTMLStyleElement ) {
35-
window.document.head.appendChild( cloned );
36-
return Promise.resolve( cloned );
37-
}
38-
const loadPromise = new Promise< HTMLLinkElement >(
39-
( resolve, reject ) => {
40-
cloned.addEventListener(
41-
'load',
42-
() => resolve( cloned ),
43-
{ once: true }
44-
);
45-
cloned.addEventListener(
46-
'error',
47-
( event ) => {
48-
const { href } =
49-
event.target as HTMLLinkElement;
50-
reject(
51-
Error(
52-
`The style sheet with the following URL failed to load. ${ href }`
53-
)
54-
);
55-
},
56-
{ once: true }
57-
);
58-
}
59-
);
60-
61-
window.document.head.appendChild( cloned );
62-
return loadPromise;
63-
}
110+
const currentStyleElements = Array.from(
111+
window.document.querySelectorAll< StyleElement >(
112+
'style,link[rel=stylesheet]'
64113
)
65114
);
66-
if ( doc !== window.document ) {
67-
window.document.head.appendChild(
68-
window.document.createComment(
69-
`@wordpress/interactivity-router: end of prefetched styles`
70-
)
71-
);
72-
}
115+
const newStyleElements = Array.from(
116+
doc.querySelectorAll< StyleElement >( 'style,link[rel=stylesheet]' )
117+
);
118+
119+
// Set styles in order.
120+
const stylePromises = updateStylesWithSCS(
121+
currentStyleElements,
122+
newStyleElements
123+
);
124+
125+
styleSheetCache.set( url, stylePromises );
73126
}
74127
return styleSheetCache.get( url );
75128
};

0 commit comments

Comments
 (0)