Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Safari broken rules #86

Merged
merged 17 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = [
{
path: 'dist/adoptedStyleSheets.js',
limit: '2 kB',
limit: '2.5 kB',
},
];
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"pretest": "rimraf .coverage",
"pretest:watch": "npm run pretest",
"pretest:coverage": "npm run pretest",
"size": "size-limit",
"size": "npm run build && size-limit",
"typecheck": "tsc --noEmit"
},
"repository": {
Expand Down
38 changes: 21 additions & 17 deletions src/ConstructedStyleSheet.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import type Location from './Location';
import {_DOMException, bootstrapper} from './shared';
import {
clearRules,
defineProperty,
insertAllRules,
rejectImports,
} from './utils';
import {fixBrokenRules, hasBrokenRules} from './safari';
import {_DOMException, bootstrapper, defineProperty} from './shared';
import {clearRules, insertAllRules, rejectImports} from './utils';

const cssStyleSheetMethods = [
'addImport',
'addPageRule',
'addRule',
'deleteRule',
'insertRule',
'removeImport',
'removeRule',
];

Expand Down Expand Up @@ -146,6 +139,7 @@ function checkInvocationCorrectness(self: ConstructedStyleSheet) {
*/
declare class ConstructedStyleSheet extends CSSStyleSheet {
replace(text: string): Promise<ConstructedStyleSheet>;

replaceSync(text: string): void;
}

Expand Down Expand Up @@ -182,7 +176,9 @@ proto.replaceSync = function replaceSync(contents) {
const self = this;

const style = $basicStyleSheet.get(self)!.ownerNode as HTMLStyleElement;
style.textContent = rejectImports(contents);
style.textContent = hasBrokenRules
? fixBrokenRules(rejectImports(contents))
: rejectImports(contents);
$basicStyleSheet.set(self, style.sheet!);

$locations.get(self)!.forEach((location) => {
Expand Down Expand Up @@ -211,12 +207,8 @@ cssStyleSheetMethods.forEach((method) => {
checkInvocationCorrectness(self);

const args = arguments;
const basic = $basicStyleSheet.get(self)!;
const locations = $locations.get(self)!;

const result = basic[method].apply(basic, args);

locations.forEach((location) => {
$locations.get(self)!.forEach((location) => {
if (location.isConnected()) {
// Type Note: If location is connected, adopter is already created; and
// since it is connected to DOM, the sheet cannot be null.
Expand All @@ -225,7 +217,19 @@ cssStyleSheetMethods.forEach((method) => {
}
});

return result;
if (hasBrokenRules) {
if (method === 'insertRule') {
args[0] = fixBrokenRules(args[0]);
}

if (method === 'addRule') {
args[1] = fixBrokenRules(args[1]);
}
}

const basic = $basicStyleSheet.get(self)!;

return basic[method].apply(basic, args);
};
});

Expand Down
150 changes: 73 additions & 77 deletions src/Location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import {
removeAdopterLocation,
restyleAdopter,
} from './ConstructedStyleSheet';
import {hasShadyCss} from './shared';
import {defineProperty, forEach, hasShadyCss} from './shared';
import {
defineProperty,
diff,
forEach,
getShadowRoot,
isElementConnected,
removeNode,
Expand Down Expand Up @@ -264,80 +262,78 @@ function Location(this: Location, element: Document | ShadowRoot) {
);
}

const proto = Location.prototype;

proto.isConnected = function isConnected() {
const element = $element.get(this)!;

return element instanceof Document
? element.readyState !== 'loading'
: isElementConnected(element.host);
};

proto.connect = function connect() {
const container = getAdopterContainer(this);

$observer.get(this)!.observe(container, defaultObserverOptions);

if ($uniqueSheets.get(this)!.length > 0) {
adopt(this);
}

traverseWebComponents(container, (root) => {
getAssociatedLocation(root).connect();
});
};

proto.disconnect = function disconnect() {
$observer.get(this)!.disconnect();
};

proto.update = function update(sheets: readonly ConstructedStyleSheet[]) {
const self = this;
const locationType =
$element.get(self) === document ? 'Document' : 'ShadowRoot';

if (!Array.isArray(sheets)) {
// document.adoptedStyleSheets = new CSSStyleSheet();
throw new TypeError(
`Failed to set the 'adoptedStyleSheets' property on ${locationType}: Iterator getter is not callable.`,
);
}

if (!sheets.every(isCSSStyleSheetInstance)) {
// document.adoptedStyleSheets = ['non-CSSStyleSheet value'];
throw new TypeError(
`Failed to set the 'adoptedStyleSheets' property on ${locationType}: Failed to convert value to 'CSSStyleSheet'`,
);
}

if (sheets.some(isNonConstructedStyleSheetInstance)) {
// document.adoptedStyleSheets = [document.styleSheets[0]];
throw new TypeError(
`Failed to set the 'adoptedStyleSheets' property on ${locationType}: Can't adopt non-constructed stylesheets`,
);
}

self.sheets = sheets;
const oldUniqueSheets = $uniqueSheets.get(self)!;
const uniqueSheets = unique(sheets);

// Style sheets that existed in the old sheet list but was excluded in the
// new one.
const removedSheets = diff(oldUniqueSheets, uniqueSheets);

removedSheets.forEach((sheet) => {
// Type Note: any removed sheet is already initialized, so there cannot be
// missing adopter for this location.
removeNode(getAdopterByLocation(sheet, self)!);
removeAdopterLocation(sheet, self);
});

$uniqueSheets.set(self, uniqueSheets);

if (self.isConnected() && uniqueSheets.length > 0) {
adopt(self);
}
// @ts-expect-error: too generic for TypeScript
Location.prototype = {
isConnected() {
const element = $element.get(this)!;

return element instanceof Document
? element.readyState !== 'loading'
: isElementConnected(element.host);
},
connect() {
const container = getAdopterContainer(this);

$observer.get(this)!.observe(container, defaultObserverOptions);

if ($uniqueSheets.get(this)!.length > 0) {
adopt(this);
}

traverseWebComponents(container, (root) => {
getAssociatedLocation(root).connect();
});
},
disconnect() {
$observer.get(this)!.disconnect();
},
update(sheets: readonly ConstructedStyleSheet[]) {
const self = this;
const locationType =
$element.get(self) === document ? 'Document' : 'ShadowRoot';

if (!Array.isArray(sheets)) {
// document.adoptedStyleSheets = new CSSStyleSheet();
throw new TypeError(
`Failed to set the 'adoptedStyleSheets' property on ${locationType}: Iterator getter is not callable.`,
);
}

if (!sheets.every(isCSSStyleSheetInstance)) {
// document.adoptedStyleSheets = ['non-CSSStyleSheet value'];
throw new TypeError(
`Failed to set the 'adoptedStyleSheets' property on ${locationType}: Failed to convert value to 'CSSStyleSheet'`,
);
}

if (sheets.some(isNonConstructedStyleSheetInstance)) {
// document.adoptedStyleSheets = [document.styleSheets[0]];
throw new TypeError(
`Failed to set the 'adoptedStyleSheets' property on ${locationType}: Can't adopt non-constructed stylesheets`,
);
}

self.sheets = sheets;
const oldUniqueSheets = $uniqueSheets.get(self)!;
const uniqueSheets = unique(sheets);

// Style sheets that existed in the old sheet list but was excluded in the
// new one.
const removedSheets = diff(oldUniqueSheets, uniqueSheets);

removedSheets.forEach((sheet) => {
// Type Note: any removed sheet is already initialized, so there cannot be
// missing adopter for this location.
removeNode(getAdopterByLocation(sheet, self)!);
removeAdopterLocation(sheet, self);
});

$uniqueSheets.set(self, uniqueSheets);

if (self.isConnected() && uniqueSheets.length > 0) {
adopt(self);
}
},
};

export default Location;
42 changes: 42 additions & 0 deletions src/safari.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {bootstrapper} from './shared';

export const hasBrokenRules = (function () {
const style = bootstrapper.createElement('style');
style.textContent = '.x{content:"y"}';
bootstrapper.body.appendChild(style);

return (style.sheet!.cssRules[0] as CSSStyleRule).style.content !== '"y"';
})();

const brokenRulePatterns = [/content:\s*["']/gm];

/**
* Adds a special symbol "%" to the broken rule that forces the internal Safari
* CSS property string converter to add quotes around the value. This function
* should be only used for the internal basic stylesheet hidden in the
* bootstrapper because it pollutes the user content with the placeholder
* symbols. Use the `getCssText` function to remove the placeholder from the
* CSS string.
*
* @param content
*/
export function fixBrokenRules(content: string): string {
return brokenRulePatterns.reduce(
(acc, pattern) => acc.replace(pattern, '$&%%%'),
content,
);
}

const placeholderPatterns = [/(content:\s*["'])%%%/gm];

/**
* Removes the placeholder added by `fixBrokenRules` function from the received
* rule string.
*/
export const getCssText = hasBrokenRules
? (rule: CSSRule) =>
placeholderPatterns.reduce(
(acc, pattern) => acc.replace(pattern, '$1'),
rule.cssText,
)
: (rule: CSSRule) => rule.cssText;
5 changes: 4 additions & 1 deletion src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const hasShadyCss = 'ShadyCSS' in window && !ShadyCSS.nativeShadow;
* The in-memory HTMLDocument that is necessary to get the internal
* CSSStyleSheet of a basic `<style>` element.
*/
export const bootstrapper = document.implementation.createHTMLDocument('boot');
export const bootstrapper = document.implementation.createHTMLDocument('');

/**
* Since ShadowRoots with the closed mode are not available via
Expand All @@ -21,3 +21,6 @@ export const closedShadowRootRegistry = new WeakMap<Element, ShadowRoot>();
// Workaround for IE that does not support the DOMException constructor
export const _DOMException =
typeof DOMException === 'object' ? Error : DOMException;

export const defineProperty = Object.defineProperty;
export const forEach = Array.prototype.forEach;
8 changes: 3 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {closedShadowRootRegistry} from './shared';

export const defineProperty = Object.defineProperty;
export const forEach = Array.prototype.forEach;
import {getCssText} from './safari';
import {closedShadowRootRegistry, forEach} from './shared';

const importPattern = /@import.+?;?$/gm;

Expand All @@ -25,7 +23,7 @@ export function clearRules(sheet: CSSStyleSheet): void {

export function insertAllRules(from: CSSStyleSheet, to: CSSStyleSheet): void {
forEach.call(from.cssRules, (rule, i) => {
to.insertRule(rule.cssText, i);
to.insertRule(getCssText(rule), i);
});
}

Expand Down