Skip to content

Commit c35de7b

Browse files
committed
Test and cleanup
1 parent a388b3a commit c35de7b

File tree

4 files changed

+74
-102
lines changed

4 files changed

+74
-102
lines changed

Diff for: src/parse.ts

-27
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ import {
4444
getAST,
4545
getSelectors,
4646
isAnchorFunction,
47-
// makeDeclarationValueUrlAbsolute,
48-
// makeImportUrlAbsolute,
4947
type StyleData,
5048
} from './utils.js';
5149
import { validatedForPositioning } from './validate.js';
@@ -684,31 +682,6 @@ export async function parseCSS(styleData: StyleData[]) {
684682
}
685683
}
686684

687-
// for (const styleObj of styleData) {
688-
// if (styleObj.changed) {
689-
// let changed = false;
690-
// const ast = getAST(styleObj.css);
691-
// // @import rules need to be reparsed, as atrule preludes are not parsed
692-
// // by default
693-
// walk(ast, {
694-
// visit: 'Atrule',
695-
// enter(node) {
696-
// changed = makeImportUrlAbsolute(node);
697-
// },
698-
// });
699-
// walk(ast, {
700-
// visit: 'Declaration',
701-
// enter(node) {
702-
// changed = makeDeclarationValueUrlAbsolute(node) || changed;
703-
// },
704-
// });
705-
// if (changed) {
706-
// // Update CSS
707-
// styleObj.css = generateCSS(ast);
708-
// }
709-
// }
710-
// }
711-
712685
// Store inline style custom property mappings for each target element
713686
const inlineStyles = new Map<HTMLElement, Record<string, string>>();
714687
// Store any `anchor()` fns

Diff for: src/transform.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import { type StyleData } from './utils.js';
22

3+
// This is a list of non-global attributes that apply to link elements but do
4+
// not apply to style elements. These should be removed when converting from a
5+
// link element to a style element. These mostly define loading behavior, which
6+
// is not relevant to style elements or our use case.
37
const excludeAttributes = [
8+
'as',
9+
'blocking',
410
'crossorigin',
11+
// 'disabled' is not relevant for style elements, but this exclusion is
12+
// theoretical, as a <link rel=stylesheet disabled> will not be loaded, and
13+
// will not reach this part of the polyfill. See #246.
14+
'disabled',
15+
'fetchpriority',
516
'href',
17+
'hreflang',
618
'integrity',
719
'referrerpolicy',
20+
'rel',
21+
'type',
822
];
923

1024
export async function transformCSS(
@@ -20,7 +34,7 @@ export async function transformCSS(
2034
// Handle inline stylesheets
2135
el.innerHTML = css;
2236
} else if (el instanceof HTMLLinkElement) {
23-
// Create new link
37+
// Replace link elements with style elements
2438
const styleEl = document.createElement('style');
2539
styleEl.textContent = css;
2640
for (const name of el.getAttributeNames()) {
@@ -31,15 +45,18 @@ export async function transformCSS(
3145
}
3246
}
3347
}
48+
// Persist the href attribute to help with potential debugging.
49+
if (el.hasAttribute('href')) {
50+
styleEl.setAttribute('data-original-href', el.getAttribute('href')!);
51+
}
3452
if (!created) {
3553
// This is an existing stylesheet, so we replace it.
3654
el.insertAdjacentElement('beforebegin', styleEl);
37-
// Wait for new stylesheet to be loaded
3855
el.remove();
3956
} else {
57+
styleEl.setAttribute('data-generated-by-polyfill', 'true');
4058
// This is a new stylesheet, so we append it.
4159
document.head.insertAdjacentElement('beforeend', styleEl);
42-
// Wait for new stylesheet to be loaded
4360
}
4461
updatedObject.el = styleEl;
4562
} else if (el.hasAttribute('data-has-inline-styles')) {

Diff for: src/utils.ts

+10-51
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import {
2-
type Atrule,
3-
type CssNode,
4-
type Declaration,
5-
type FunctionNode,
6-
type Identifier,
1+
import type {
2+
CssNode,
3+
Declaration,
4+
FunctionNode,
5+
Identifier,
76
List,
8-
type Selector as CssTreeSelector,
9-
type SelectorList,
10-
type StyleSheet,
11-
type Value,
7+
Selector as CssTreeSelector,
8+
SelectorList,
9+
Value,
1210
} from 'css-tree';
1311
import generate from 'css-tree/generator';
1412
import parse from 'css-tree/parser';
@@ -29,9 +27,9 @@ export function isAnchorFunction(node: CssNode | null): node is FunctionNode {
2927
return Boolean(node && node.type === 'Function' && node.name === 'anchor');
3028
}
3129

32-
export function getAST(cssText: string, parseAtrulePrelude = false) {
30+
export function getAST(cssText: string) {
3331
return parse(cssText, {
34-
parseAtrulePrelude: parseAtrulePrelude,
32+
parseAtrulePrelude: false,
3533
parseCustomProperty: true,
3634
});
3735
}
@@ -52,45 +50,6 @@ export function getDeclarationValue(node: DeclarationWithValue) {
5250
return (node.value.children.first as Identifier).name;
5351
}
5452

55-
function isImportRule(node: CssNode): node is Atrule {
56-
return node.type === 'Atrule' && node.name === 'import';
57-
}
58-
59-
export function makeImportUrlAbsolute(node: CssNode): boolean {
60-
if (!isImportRule(node)) return false;
61-
// AtRulePreludes are not parsed by default, so we need to reparse the node
62-
// to get the URL.
63-
const reparsedNode = (getAST(generateCSS(node), true) as StyleSheet).children
64-
.first;
65-
if (!reparsedNode || !isImportRule(reparsedNode)) return false;
66-
if (reparsedNode.prelude?.type !== 'AtrulePrelude') return false;
67-
68-
// URL is always the first child of the prelude
69-
const url = reparsedNode.prelude.children.first;
70-
if (!url) return false;
71-
if (url.type !== 'Url' && url.type !== 'String') return false;
72-
url.value = new URL(url.value, document.baseURI).href;
73-
node.prelude = reparsedNode.prelude;
74-
return true;
75-
}
76-
77-
export function makeDeclarationValueUrlAbsolute(node: CssNode): boolean {
78-
if (!isDeclaration(node)) return false;
79-
80-
if (node.value.type !== 'Value') return false;
81-
let changed = false;
82-
83-
const mapped = node.value.children.toArray().map((child) => {
84-
if (!child) return child;
85-
if (child.type !== 'Url') return child;
86-
child.value = new URL(child.value, document.baseURI).href;
87-
changed = true;
88-
return child;
89-
});
90-
node.value.children = new List<CssNode>().fromArray(mapped);
91-
return changed;
92-
}
93-
9453
export interface StyleData {
9554
el: HTMLElement;
9655
css: string;

Diff for: tests/unit/transform.test.ts

+44-21
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { transformCSS } from '../../src/transform.js';
22

33
describe('transformCSS', () => {
4-
beforeAll(() => {
5-
global.URL.createObjectURL = vi.fn().mockReturnValue('/updated.css');
6-
});
7-
84
it('parses and removes new anchor positioning CSS after transformation to JS', async () => {
95
document.head.innerHTML = `
106
<link type="text/css" href="/sample.css" data-link="true" crossorigin="anonymous" />
@@ -16,7 +12,7 @@ describe('transformCSS', () => {
1612
<div id="div" data-has-inline-styles="key" style="--foo: var(--bar); color: red;" />
1713
<div id="div2" data-has-inline-styles="key2" style="color: red;" />
1814
`;
19-
let link = document.querySelector('link') as HTMLLinkElement;
15+
const link = document.querySelector('link') as HTMLLinkElement;
2016
const style = document.querySelector('style') as HTMLStyleElement;
2117
const div = document.getElementById('div') as HTMLDivElement;
2218
const div2 = document.getElementById('div2') as HTMLDivElement;
@@ -36,14 +32,16 @@ describe('transformCSS', () => {
3632
];
3733
const inlineStyles = new Map();
3834
inlineStyles.set(div, { '--foo': '--bar' });
39-
const promise = transformCSS(styleData, inlineStyles, true);
40-
link = document.querySelector('link') as HTMLLinkElement;
41-
link.dispatchEvent(new Event('load'));
42-
await promise;
35+
await transformCSS(styleData, inlineStyles, true);
36+
37+
expect(link.isConnected).toBe(false);
38+
const newLink = document.querySelector(
39+
'style[data-original-href]',
40+
) as HTMLStyleElement;
41+
expect(newLink.getAttribute('data-link')).toBe('true');
42+
expect(newLink.getAttribute('crossorigin')).toBeNull();
43+
expect(newLink.textContent).toBe('html { margin: 0; }');
4344

44-
expect(link.href).toContain('/updated.css');
45-
expect(link.getAttribute('data-link')).toBe('true');
46-
expect(link.getAttribute('crossorigin')).toBeNull();
4745
expect(style.innerHTML).toBe('html { padding: 0; }');
4846
expect(div.getAttribute('style')).toBe('--foo: var(--bar); color:blue;');
4947
expect(div2.getAttribute('style')).toBe('color: red;');
@@ -55,17 +53,42 @@ describe('transformCSS', () => {
5553
document.head.innerHTML = `
5654
<link id="the-link" media="screen" title="stylish" rel="stylesheet" href="/sample.css"/>
5755
`;
58-
let link = document.querySelector('link') as HTMLLinkElement;
56+
const link = document.querySelector('link') as HTMLLinkElement;
5957
const styleData = [{ el: link, css: 'html { margin: 0; }', changed: true }];
6058
const inlineStyles = new Map();
61-
const promise = transformCSS(styleData, inlineStyles, true);
62-
link = document.querySelector('link') as HTMLLinkElement;
63-
link.dispatchEvent(new Event('load'));
64-
await promise;
59+
const initialStyleElement = document.querySelector('style');
60+
expect(initialStyleElement).toBe(null);
61+
await transformCSS(styleData, inlineStyles, true);
62+
const transformedStyleElement = document.querySelector(
63+
'style',
64+
) as HTMLStyleElement;
65+
expect(transformedStyleElement.id).toBe('the-link');
66+
expect(transformedStyleElement.media).toBe('screen');
67+
expect(transformedStyleElement.title).toBe('stylish');
68+
69+
const transformedLink = document.querySelector('link') as HTMLLinkElement;
70+
expect(transformedLink).toBe(null);
71+
});
72+
73+
it('creates new style elements for created styles', async () => {
74+
document.head.innerHTML = ``;
75+
const styleData = [
76+
{
77+
el: document.createElement('link'),
78+
css: 'html { margin: 0; }',
79+
changed: true,
80+
created: true,
81+
},
82+
];
83+
await transformCSS(styleData, undefined, true);
6584

66-
expect(link.href).toContain('/updated.css');
67-
expect(link.id).toBe('the-link');
68-
expect(link.media).toBe('screen');
69-
expect(link.title).toBe('stylish');
85+
const createdStyleElement = document.querySelector(
86+
'style',
87+
) as HTMLStyleElement;
88+
expect(createdStyleElement.hasAttribute('data-original-href')).toBe(false);
89+
expect(createdStyleElement.hasAttribute('data-generated-by-polyfill')).toBe(
90+
true,
91+
);
92+
expect(createdStyleElement.textContent).toBe('html { margin: 0; }');
7093
});
7194
});

0 commit comments

Comments
 (0)