react-native-browser-with-polyfill 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1859 +0,0 @@
1
- // ==UserScript==
2
- // @name iPadOS 15 Safari Polyfill Suite
3
- // @namespace https://github.com/huyquoc/ipados15-polyfill
4
- // @version 1.6.4
5
- // @description Comprehensive JS and CSS polyfills for iPadOS 15 Safari (WebKit 15.x). Fixes broken layouts on modern sites.
6
- // @author Huy (Huy Quoc)
7
- // @match *://*/*
8
- // @grant none
9
- // @run-at document-start
10
- // @license MIT
11
- // ==/UserScript==
12
-
13
- /*
14
- * =====================================================================
15
- * iPadOS 15 Safari Polyfill Suite v1.4.0
16
- * =====================================================================
17
- *
18
- * Polyfills JavaScript and CSS features missing from Safari 15.x
19
- * (equivalent to Safari 15.0-15.5 shipped with iPadOS 15).
20
- *
21
- * JS features polyfilled:
22
- * - globalThis
23
- * - Object.hasOwn()
24
- * - Object.groupBy() / Map.groupBy()
25
- * - Array.fromAsync()
26
- * - Array.prototype.at()
27
- * - Array.prototype.findLast() / findLastIndex()
28
- * - String.prototype.replaceAll()
29
- * - Element.prototype.replaceAll()
30
- * - Element.prototype.checkVisibility()
31
- * - Promise.any() / AggregateError
32
- * - Promise.withResolvers()
33
- * - Set methods: union, intersection, difference, isSubsetOf, isSupersetOf, isDisjointFrom
34
- * - URL.canParse()
35
- * - AbortSignal.timeout()
36
- * - AbortSignal.any()
37
- * - crypto.randomUUID()
38
- * - structuredClone()
39
- * - Iterator helpers: Iterator.from(), Iterator.prototype.map(), filter(), take(), drop(), forEach(), toArray()
40
- * - CSS reset for computed fallbacks
41
- *
42
- * CSS features polyfilled:
43
- * - :has() pseudo-class → JS-based parent selector
44
- * - @container rules → rewritten to @media or JS-calculated
45
- * - CSS nesting syntax → flattened to nested selectors
46
- * - oklch() / oklab() → converted to lab()/rgb()
47
- * - color-mix() → JS color interpolation
48
- * - dvh / svh / lvh / dvw / svw / lvw → viewport-calc fallbacks
49
- * - text-wrap: balance → JS text balancing fallback
50
- * - subgrid → IE-style fallback
51
- * - CSS calc with unsupported units
52
- *
53
- * =====================================================================
54
- */
55
-
56
- (function () {
57
- "use strict";
58
-
59
- // ── Guards and diagnostics ──────────────────────────────────────────
60
- const POLYFILL_NS = "ipados15-polyfill";
61
- const _ua = navigator.userAgent;
62
- const _isSafari = /Safari/.test(_ua) && !/Chrome/.test(_ua);
63
- const _isIPadOS15 =
64
- /iPad/.test(_ua) ||
65
- (/Macintosh/.test(_ua) && navigator.maxTouchPoints > 1);
66
-
67
- // Log diagnostics
68
- var _applied = [];
69
- function _log(feature, status) {
70
- status = status || "polyfilled";
71
- _applied.push(feature + " [" + status + "]");
72
- console.log("[%s] %s: %s", POLYFILL_NS, feature, status);
73
- }
74
- function _unavailable(feature, reason) {
75
- _applied.push(feature + " [skipped: " + reason + "]");
76
- }
77
-
78
- // Safe wrapper: runs fn in try/catch, logs error but never crashes the script
79
- function _safe(feature, fn) {
80
- try {
81
- fn();
82
- } catch (e) {
83
- _log(feature, "error: " + e.message);
84
- }
85
- }
86
-
87
- // Skip on non-iPadOS-15 but still polyfill if feature is actually missing
88
- // (user may be on desktop Safari 15 too)
89
- var _shouldSkip =
90
- !_isSafari &&
91
- !/Version\/15\./.test(_ua) &&
92
- !/Version\/16\./.test(_ua) &&
93
- !/Version\/17\./.test(_ua);
94
-
95
- // ── 0. globalThis ──────────────────────────────────────────────────
96
- if (typeof globalThis === "undefined") {
97
- try {
98
- var _globalObj;
99
- if (typeof self !== "undefined") {
100
- _globalObj = self;
101
- } else if (typeof window !== "undefined") {
102
- _globalObj = window;
103
- } else if (typeof global !== "undefined") {
104
- _globalObj = global;
105
- } else {
106
- _globalObj = Function("return this")();
107
- }
108
- Object.defineProperty(_globalObj, "globalThis", {
109
- value: _globalObj,
110
- configurable: true,
111
- writable: true,
112
- });
113
- _log("globalThis", "polyfilled");
114
- } catch (e) {
115
- _log("globalThis", "error: " + e.message);
116
- }
117
- }
118
-
119
- // ── 1. String.prototype.replaceAll ─────────────────────────────────
120
- if (typeof String.prototype.replaceAll !== "function") {
121
- String.prototype.replaceAll = function (search, replacement) {
122
- var target = String(this);
123
- var searchStr = String(search);
124
- if (searchStr === "") {
125
- // Edge case: replacing empty string inserts between every char
126
- return target.split("").join(replacement) + replacement;
127
- }
128
- // Escape regex special chars if search is a string
129
- var escaped = searchStr.replace(
130
- /[.*+?^${}()|[\]\\]/g,
131
- "\\$&"
132
- );
133
- var regex = new RegExp(escaped, "g");
134
- return target.replace(regex, replacement);
135
- };
136
- _log("String.replaceAll", "polyfilled");
137
- }
138
-
139
- // ── 2. Object.hasOwn ───────────────────────────────────────────────
140
- if (typeof Object.hasOwn !== "function") {
141
- Object.hasOwn = function (obj, prop) {
142
- if (obj == null) throw new TypeError("Cannot convert undefined/null");
143
- return Object.prototype.hasOwnProperty.call(obj, prop);
144
- };
145
- _log("Object.hasOwn", "polyfilled");
146
- }
147
-
148
- // ── 3. Object.groupBy / Map.groupBy ────────────────────────────────
149
- if (typeof Object.groupBy !== "function") {
150
- Object.groupBy = function (collection, callback) {
151
- var result = {};
152
- // Handle iterables and array-like
153
- if (Symbol.iterator in Object(collection)) {
154
- for (var item of collection) {
155
- var key = callback(item, 0);
156
- result[key] = result[key] || [];
157
- result[key].push(item);
158
- }
159
- } else {
160
- var idx = 0;
161
- for (var key in collection) {
162
- if (Object.prototype.hasOwnProperty.call(collection, key)) {
163
- var val = collection[key];
164
- var groupKey = callback(val, idx);
165
- result[groupKey] = result[groupKey] || [];
166
- result[groupKey].push(val);
167
- idx++;
168
- }
169
- }
170
- }
171
- return result;
172
- };
173
- _log("Object.groupBy", "polyfilled");
174
- }
175
-
176
- if (typeof Map.groupBy !== "function") {
177
- Map.groupBy = function (collection, callback) {
178
- var result = new Map();
179
- if (Symbol.iterator in Object(collection)) {
180
- for (var item of collection) {
181
- var key = callback(item, 0);
182
- if (!result.has(key)) result.set(key, []);
183
- result.get(key).push(item);
184
- }
185
- }
186
- return result;
187
- };
188
- _log("Map.groupBy", "polyfilled");
189
- }
190
-
191
- // ── 4. Array.fromAsync ─────────────────────────────────────────────
192
- if (typeof Array.fromAsync !== "function") {
193
- Array.fromAsync = function (asyncIterable, mapFn) {
194
- var result = [];
195
- return (async function () {
196
- try {
197
- var iterator =
198
- asyncIterable[Symbol.asyncIterator] ||
199
- (asyncIterable[Symbol.iterator]
200
- ? (function (iter) {
201
- return {
202
- next: function () {
203
- var r = iter.next();
204
- return Promise.resolve(r);
205
- },
206
- };
207
- })(asyncIterable[Symbol.iterator]())
208
- : null);
209
- if (!iterator)
210
- throw new TypeError(
211
- "Object is not async-iterable"
212
- );
213
- var step;
214
- while (!(step = await iterator.next()).done) {
215
- var value = step.value;
216
- if (mapFn) value = mapFn(value);
217
- result.push(value);
218
- }
219
- } catch (e) {
220
- try {
221
- await iterator.return();
222
- } catch (_) {}
223
- throw e;
224
- }
225
- return result;
226
- })();
227
- };
228
- _log("Array.fromAsync", "polyfilled");
229
- }
230
-
231
- // ── 5. Array.prototype.at ─────────────────────────────────────────
232
- _safe("Array.at", function() {
233
- if (typeof Array.prototype.at !== "function") {
234
- Array.prototype.at = function (index) {
235
- var arr = this;
236
- var len = arr.length || 0;
237
- if (len === 0) return undefined;
238
- if (index < 0) index = len + index;
239
- if (index < 0 || index >= len) return undefined;
240
- return arr[index];
241
- };
242
- _log("Array.at", "polyfilled");
243
- }
244
- });
245
-
246
- // ── 5b. String.prototype.at ────────────────────────────────────────
247
- _safe("String.at", function() {
248
- if (typeof String.prototype.at !== "function") {
249
- String.prototype.at = function (index) {
250
- var str = String(this);
251
- var len = str.length;
252
- if (len === 0) return undefined;
253
- if (index < 0) index = len + index;
254
- if (index < 0 || index >= len) return undefined;
255
- return str[index];
256
- };
257
- _log("String.at", "polyfilled");
258
- }
259
- });
260
-
261
- // ── 5c. TypedArray.prototype.at ────────────────────────────────────
262
- _safe("TypedArray.at", function() {
263
- var TypedArrays = [
264
- typeof Int8Array !== "undefined" && Int8Array,
265
- typeof Uint8Array !== "undefined" && Uint8Array,
266
- typeof Uint8ClampedArray !== "undefined" && Uint8ClampedArray,
267
- typeof Int16Array !== "undefined" && Int16Array,
268
- typeof Uint16Array !== "undefined" && Uint16Array,
269
- typeof Int32Array !== "undefined" && Int32Array,
270
- typeof Uint32Array !== "undefined" && Uint32Array,
271
- typeof Float32Array !== "undefined" && Float32Array,
272
- typeof Float64Array !== "undefined" && Float64Array,
273
- ].filter(Boolean);
274
- var atAdded = false;
275
- for (var i = 0; i < TypedArrays.length; i++) {
276
- var TA = TypedArrays[i];
277
- if (typeof TA.prototype.at !== "function") {
278
- TA.prototype.at = function (index) {
279
- var len = this.length;
280
- if (len === 0) return undefined;
281
- if (index < 0) index = len + index;
282
- if (index < 0 || index >= len) return undefined;
283
- return this[index];
284
- };
285
- atAdded = true;
286
- }
287
- }
288
- if (atAdded) _log("TypedArray.at", "polyfilled");
289
- });
290
-
291
- // ── 6. Array.prototype.findLast / findLastIndex ────────────────────
292
- if (typeof Array.prototype.findLast !== "function") {
293
- Array.prototype.findLast = function (predicate, thisArg) {
294
- for (var i = this.length - 1; i >= 0; i--) {
295
- if (predicate.call(thisArg, this[i], i, this)) return this[i];
296
- }
297
- return undefined;
298
- };
299
- _log("Array.findLast", "polyfilled");
300
- }
301
-
302
- if (typeof Array.prototype.findLastIndex !== "function") {
303
- Array.prototype.findLastIndex = function (predicate, thisArg) {
304
- for (var i = this.length - 1; i >= 0; i--) {
305
- if (predicate.call(thisArg, this[i], i, this)) return i;
306
- }
307
- return -1;
308
- };
309
- _log("Array.findLastIndex", "polyfilled");
310
- }
311
-
312
- // ── 7. Element.prototype.replaceAll ────────────────────────────────
313
- if (typeof Element.prototype.replaceAll !== "function") {
314
- Element.prototype.replaceAll = function (newChild) {
315
- var parent = this.parentNode;
316
- if (!parent) return this;
317
- if (typeof newChild === "string") {
318
- // Create a document fragment from the HTML string
319
- var fragment = document.createDocumentFragment();
320
- var temp = document.createElement("template");
321
- temp.innerHTML = newChild.trim();
322
- while (temp.content.firstChild) {
323
- fragment.appendChild(temp.content.firstChild);
324
- }
325
- parent.replaceChild(fragment, this);
326
- } else {
327
- parent.replaceChild(newChild, this);
328
- }
329
- return this;
330
- };
331
- _log("Element.replaceAll", "polyfilled");
332
- }
333
-
334
- // ── 8. Element.prototype.checkVisibility ───────────────────────────
335
- if (typeof Element.prototype.checkVisibility !== "function") {
336
- Element.prototype.checkVisibility = function (opts) {
337
- var o = opts || {};
338
- var checkOpacity = o.checkOpacity !== false;
339
- var checkVisibilityCSS = o.checkVisibilityCSS !== false;
340
- var el = this;
341
- if (!checkVisibilityCSS) {
342
- // If we don't need to check parents, just check this element
343
- } else {
344
- el = this.closest(
345
- ":not([hidden]), :not([style*='display: none'])"
346
- );
347
- }
348
- if (!el) return false;
349
- var style = getComputedStyle(el);
350
- return (
351
- style.display !== "none" &&
352
- style.visibility !== "hidden" &&
353
- style.visibility !== "collapse" &&
354
- (checkOpacity === false || style.opacity !== "0")
355
- );
356
- };
357
- _log("Element.checkVisibility", "polyfilled");
358
- }
359
-
360
- // ── 9. Promise.any() / AggregateError ──────────────────────────────
361
- _safe("AggregateError", function() {
362
- if (typeof AggregateError !== "function") {
363
- var AggregateError = function (errors, message) {
364
- if (!(this instanceof AggregateError))
365
- return new AggregateError(errors, message);
366
- if (typeof Error.captureStackTrace === "function") {
367
- Error.captureStackTrace(this, AggregateError);
368
- }
369
- this.name = "AggregateError";
370
- this.errors = Array.isArray(errors) ? errors : [errors];
371
- this.message = message || "";
372
- };
373
- AggregateError.prototype = Object.create(Error.prototype);
374
- AggregateError.prototype.constructor = AggregateError;
375
- // Avoid redefining if global exists but is a class
376
- if (typeof window.AggregateError === "undefined") {
377
- window.AggregateError = AggregateError;
378
- }
379
- }
380
-
381
- if (typeof Promise.any !== "function") {
382
- Promise.any = function (iterable) {
383
- var promises = Array.from(iterable);
384
- return new Promise(function (resolve, reject) {
385
- if (promises.length === 0) {
386
- var err = new AggregateError(
387
- [],
388
- "All promises were rejected"
389
- );
390
- reject(err);
391
- return;
392
- }
393
- var errors = [];
394
- var settled = 0;
395
- promises.forEach(function (p, i) {
396
- Promise.resolve(p)
397
- .then(resolve)
398
- .catch(function (err) {
399
- errors[i] = err;
400
- settled++;
401
- if (settled === promises.length) {
402
- reject(new AggregateError(errors, "All promises rejected"));
403
- }
404
- });
405
- });
406
- });
407
- };
408
- _log("Promise.any", "polyfilled");
409
- }
410
- }); // end _safe AggregateError
411
-
412
- // ── 10. Promise.withResolvers ──────────────────────────────────────
413
- if (typeof Promise.withResolvers !== "function") {
414
- Promise.withResolvers = function () {
415
- var resolve, reject;
416
- var promise = new Promise(function (res, rej) {
417
- resolve = res;
418
- reject = rej;
419
- });
420
- return { promise: promise, resolve: resolve, reject: reject };
421
- };
422
- _log("Promise.withResolvers", "polyfilled");
423
- }
424
-
425
- // ── 11. Set methods (union, intersection, difference, etc.) ────────
426
- _safe("Set.methods", function() {
427
- if (typeof Set.prototype.union !== "function") {
428
- Set.prototype.union = function (otherSet) {
429
- return new Set([...this, ...otherSet]);
430
- };
431
- _log("Set.union", "polyfilled");
432
- }
433
-
434
- if (typeof Set.prototype.intersection !== "function") {
435
- Set.prototype.intersection = function (otherSet) {
436
- return new Set(
437
- [...this].filter(function (item) {
438
- return otherSet.has(item);
439
- })
440
- );
441
- };
442
- _log("Set.intersection", "polyfilled");
443
- }
444
-
445
- if (typeof Set.prototype.difference !== "function") {
446
- Set.prototype.difference = function (otherSet) {
447
- return new Set(
448
- [...this].filter(function (item) {
449
- return !otherSet.has(item);
450
- })
451
- );
452
- };
453
- _log("Set.difference", "polyfilled");
454
- }
455
-
456
- if (typeof Set.prototype.isSubsetOf !== "function") {
457
- Set.prototype.isSubsetOf = function (otherSet) {
458
- if (this.size > otherSet.size) return false;
459
- return [...this].every(function (item) {
460
- return otherSet.has(item);
461
- });
462
- };
463
- _log("Set.isSubsetOf", "polyfilled");
464
- }
465
-
466
- if (typeof Set.prototype.isSupersetOf !== "function") {
467
- Set.prototype.isSupersetOf = function (otherSet) {
468
- return otherSet.isSubsetOf(this);
469
- };
470
- _log("Set.isSupersetOf", "polyfilled");
471
- }
472
-
473
- if (typeof Set.prototype.isDisjointFrom !== "function") {
474
- Set.prototype.isDisjointFrom = function (otherSet) {
475
- return [...this].every(function (item) {
476
- return !otherSet.has(item);
477
- });
478
- };
479
- _log("Set.isDisjointFrom", "polyfilled");
480
- }
481
- }); // end _safe Set.methods
482
-
483
- // ── 12. URL.canParse / URL.canParseBase / URL.canParseHref ─────────
484
- if (typeof URL.canParse !== "function") {
485
- URL.canParse = function (url, base) {
486
- try {
487
- new URL(String(url), base ? String(base) : undefined);
488
- return true;
489
- } catch (e) {
490
- return false;
491
- }
492
- };
493
- _log("URL.canParse", "polyfilled");
494
- }
495
-
496
- // ── 13. AbortSignal.timeout ────────────────────────────────────────
497
- if (typeof AbortSignal.timeout !== "function") {
498
- AbortSignal.timeout = function (milliseconds) {
499
- var controller = new AbortController();
500
- setTimeout(function () {
501
- controller.abort(new DOMException("TimeoutError", "TimeoutError"));
502
- }, milliseconds);
503
- return controller.signal;
504
- };
505
- _log("AbortSignal.timeout", "polyfilled");
506
- }
507
-
508
- // ── 14. AbortSignal.any ────────────────────────────────────────────
509
- if (typeof AbortSignal.any !== "function") {
510
- AbortSignal.any = function (signals) {
511
- if (!Array.isArray(signals) || signals.length === 0) {
512
- var solo = new AbortController();
513
- return solo.signal;
514
- }
515
- if (signals.length === 1) return signals[0];
516
-
517
- var controller = new AbortController();
518
- signals.forEach(function (sig) {
519
- if (sig.aborted) {
520
- controller.abort(sig.reason);
521
- return;
522
- }
523
- try {
524
- sig.addEventListener(
525
- "abort",
526
- function () {
527
- controller.abort(sig.reason);
528
- },
529
- { once: true }
530
- );
531
- } catch (_) {
532
- // Some signals may not support addEventListener (older browsers)
533
- }
534
- });
535
- return controller.signal;
536
- };
537
- _log("AbortSignal.any", "polyfilled");
538
- }
539
-
540
- // ── 15. crypto.randomUUID ──────────────────────────────────────────
541
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID !== "function") {
542
- // Use Math.random-based fallback (not cryptographically secure but functional)
543
- crypto.randomUUID = function () {
544
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
545
- /[xy]/g,
546
- function (c) {
547
- var r = (Math.random() * 16) | 0;
548
- var v = c === "x" ? r : (r & 0x3) | 0x8;
549
- return v.toString(16);
550
- }
551
- );
552
- };
553
- _log("crypto.randomUUID", "polyfilled");
554
- }
555
-
556
- // ── 16. structuredClone ────────────────────────────────────────────
557
- if (typeof structuredClone !== "function") {
558
- var _structuredClone = function (value) {
559
- return _structuredCloneInternal(value, new WeakMap());
560
- };
561
- if (typeof globalThis.structuredClone === "undefined") {
562
- globalThis.structuredClone = _structuredClone;
563
- }
564
- function _structuredCloneInternal(value, seen) {
565
- if (value === null || typeof value !== "object") return value;
566
- // Handle circular reference
567
- if (seen.has(value)) {
568
- throw new TypeError("Converting circular structure to structured clone");
569
- }
570
- // Handle Date
571
- if (value instanceof Date) return new Date(value.getTime());
572
- // Handle RegExp
573
- if (value instanceof RegExp) return new RegExp(value.source, value.flags);
574
- // Handle Array
575
- if (Array.isArray(value)) {
576
- var arr = [];
577
- seen.set(value, arr);
578
- for (var i = 0; i < value.length; i++) {
579
- arr.push(_structuredCloneInternal(value[i], seen));
580
- }
581
- return arr;
582
- }
583
- // Handle Map
584
- if (value instanceof Map) {
585
- var map = new Map();
586
- seen.set(value, map);
587
- value.forEach(function (v, k) {
588
- map.set(
589
- typeof k === "object" && k !== null ? _structuredCloneInternal(k, seen) : k,
590
- _structuredCloneInternal(v, seen)
591
- );
592
- });
593
- return map;
594
- }
595
- // Handle Set
596
- if (value instanceof Set) {
597
- var set = new Set();
598
- seen.set(value, set);
599
- value.forEach(function (v) {
600
- set.add(_structuredCloneInternal(v, seen));
601
- });
602
- return set;
603
- }
604
- // Handle ArrayBuffer
605
- if (value instanceof ArrayBuffer) {
606
- return value.slice(0);
607
- }
608
- // Handle TypedArrays
609
- if (ArrayBuffer.isView(value)) {
610
- return value.slice(0);
611
- }
612
- // Handle plain objects
613
- if (value instanceof Object) {
614
- var obj = {};
615
- seen.set(value, obj);
616
- var keys = Object.keys(value);
617
- for (var i = 0; i < keys.length; i++) {
618
- obj[keys[i]] = _structuredCloneInternal(value[keys[i]], seen);
619
- }
620
- return obj;
621
- }
622
- return value;
623
- }
624
- _log("structuredClone", "polyfilled");
625
- }
626
-
627
- // ── 17. Iterator helpers ───────────────────────────────────────────
628
- _safe("Iterator", function() {
629
- if (typeof Iterator === "undefined" || typeof Iterator.from !== "function") {
630
- // Polyfill Iterator.from()
631
- var IteratorPolyfill = function (iterable) {
632
- var iterator;
633
- if (iterable == null)
634
- throw new TypeError("iterator is null or undefined");
635
- if (typeof iterable[Symbol.iterator] === "function") {
636
- iterator = iterable[Symbol.iterator]();
637
- if (!iterator || typeof iterator.next !== "function")
638
- throw new TypeError("iterator is not a sequence");
639
- } else if (typeof iterable[Symbol.asyncIterator] === "function") {
640
- // Async iterator handling
641
- var asyncIter = iterable[Symbol.asyncIterator]();
642
- iterator = {
643
- next: function () {
644
- return asyncIter.next().then(function (result) {
645
- return { value: result.value, done: result.done };
646
- });
647
- },
648
- [Symbol.asyncIterator]: function () {
649
- return this;
650
- },
651
- };
652
- } else {
653
- throw new TypeError("object is not iterable");
654
- }
655
- // Check if iterator is already an Iterator (has iterator helpers)
656
- if (typeof iterator[Symbol.iterator] === "function" && iterator[Symbol.iterator]()) {
657
- var inner = iterator[Symbol.iterator]();
658
- if (typeof inner.next === "function" && typeof inner[Symbol.iterator] === "function") {
659
- // Already has iterator protocol
660
- }
661
- }
662
- return iterator;
663
- };
664
-
665
- var Iterator;
666
- if (typeof globalThis.Iterator === "undefined") {
667
- var IteratorClass = function (iterable) {
668
- return IteratorPolyfill(iterable);
669
- };
670
- IteratorClass.from = IteratorPolyfill;
671
- Iterator = IteratorClass;
672
- globalThis.Iterator = Iterator;
673
- } else {
674
- Iterator = globalThis.Iterator;
675
- Iterator.from = IteratorPolyfill;
676
- }
677
- _log("Iterator.from", "polyfilled");
678
- }
679
-
680
- // Add Iterator prototype methods (map, filter, take, drop, forEach, toArray)
681
- if (typeof globalThis.Iterator !== "undefined") {
682
- var IteratorProto = globalThis.Iterator.prototype;
683
-
684
- // Add a base Symbol.iterator so we can detect our polyfilled iterators
685
- if (typeof IteratorProto[Symbol.iterator] !== "function") {
686
- IteratorProto[Symbol.iterator] = function () {
687
- return this;
688
- };
689
- }
690
-
691
- // Already-have-map check: only add if not present
692
- if (typeof IteratorProto.map !== "function") {
693
- IteratorProto.map = function (fn, thisArg) {
694
- var self = this;
695
- var idx = 0;
696
- return {
697
- next: function () {
698
- var result = self.next();
699
- if (result.done) return result;
700
- result.value = fn.call(thisArg, result.value, idx++);
701
- return result;
702
- },
703
- [Symbol.iterator]: function () {
704
- return this;
705
- },
706
- [Symbol.asyncIterator]: function () {
707
- return this;
708
- },
709
- };
710
- };
711
- }
712
-
713
- if (typeof IteratorProto.filter !== "function") {
714
- IteratorProto.filter = function (predicate, thisArg) {
715
- var self = this;
716
- var idx = 0;
717
- return {
718
- next: function () {
719
- var result;
720
- while (!(result = self.next()).done) {
721
- if (predicate.call(thisArg, result.value, idx++)) {
722
- return result;
723
- }
724
- }
725
- return result;
726
- },
727
- [Symbol.iterator]: function () {
728
- return this;
729
- },
730
- [Symbol.asyncIterator]: function () {
731
- return this;
732
- },
733
- };
734
- };
735
- }
736
-
737
- if (typeof IteratorProto.take !== "function") {
738
- IteratorProto.take = function (n) {
739
- var self = this;
740
- var count = 0;
741
- return {
742
- next: function () {
743
- if (count >= n) return { done: true, value: undefined };
744
- var result = self.next();
745
- count++;
746
- return result;
747
- },
748
- [Symbol.iterator]: function () {
749
- return this;
750
- },
751
- [Symbol.asyncIterator]: function () {
752
- return this;
753
- },
754
- };
755
- };
756
- }
757
-
758
- if (typeof IteratorProto.drop !== "function") {
759
- IteratorProto.drop = function (n) {
760
- var self = this;
761
- var i = 0;
762
- while (i < n) {
763
- var skip = self.next();
764
- if (skip.done) return { next: function () { return { done: true, value: undefined }; }, [Symbol.iterator]: function () { return this; }, [Symbol.asyncIterator]: function () { return this; } };
765
- i++;
766
- }
767
- return self;
768
- };
769
- }
770
-
771
- if (typeof IteratorProto.forEach !== "function") {
772
- IteratorProto.forEach = function (fn, thisArg) {
773
- var idx = 0;
774
- var result;
775
- while (!(result = this.next()).done) {
776
- fn.call(thisArg, result.value, idx++);
777
- }
778
- };
779
- }
780
-
781
- if (typeof IteratorProto.toArray !== "function") {
782
- IteratorProto.toArray = function () {
783
- var arr = [];
784
- var result;
785
- while (!(result = this.next()).done) {
786
- arr.push(result.value);
787
- }
788
- return arr;
789
- };
790
- }
791
-
792
- _log("Iterator helpers (map/filter/take/drop/forEach/toArray)", "polyfilled");
793
- }
794
- }); // end _safe Iterator
795
-
796
- // ── 18. CSS Polyfill: Viewport height units (dvh, svh, lvh) ────────
797
- // iPadOS 15 doesn't support dvh/svh/lvw. Inject CSS custom properties.
798
- (function polyfillViewportUnits() {
799
- function _updateViewport() {
800
- try {
801
- var vh = window.innerHeight || document.documentElement.clientHeight;
802
- var vw = window.innerWidth || document.documentElement.clientWidth;
803
-
804
- // dvh: dynamic viewport height (excludes toolbar)
805
- // In Safari 15, dynamic viewport ~= visual viewport = innerHeight
806
- var dvh = vh + "px";
807
-
808
- // svh: smallest viewport height
809
- // In Safari 15, smallest viewport ~= innerHeight
810
- var svh = vh + "px";
811
-
812
- // lvh: largest viewport height
813
- // In Safari 15, largest viewport can be larger than screen height
814
- // We estimate using document height as the largest
815
- var lvh = Math.max(vh, document.documentElement.scrollHeight) + "px";
816
-
817
- // dvw / svw / lvw
818
- var dvw = vw + "px";
819
- var svw = vw + "px";
820
- var lvw = Math.max(vw, document.documentElement.scrollWidth) + "px";
821
-
822
- // Inject or update CSS custom properties
823
- var style = document.getElementById("ipados15-viewport-units");
824
- if (!style) {
825
- style = document.createElement("style");
826
- style.setAttribute("data-ipados15-polyfill", "true");
827
- style.setAttribute("id", "ipados15-viewport-units");
828
- document.head.appendChild(style);
829
- }
830
- style.textContent =
831
- ":root{\n" +
832
- "--ipados15-dvh:" + dvh + ";\n" +
833
- "--ipados15-svh:" + svh + ";\n" +
834
- "--ipados15-lvh:" + lvh + ";\n" +
835
- "--ipados15-dvw:" + dvw + ";\n" +
836
- "--ipados15-svw:" + svw + ";\n" +
837
- "--ipados15-lvw:" + lvw + ";\n" +
838
- "}\n";
839
- _log("Viewport units (dvh/svh/lvh → --ipados15-* var)", "polyfilled");
840
- } catch (e) {
841
- _log("Viewport units", "error: " + e.message);
842
- }
843
- }
844
-
845
- _updateViewport();
846
-
847
- // Update viewport properties on resize and orientation change
848
- var _viewportResizeTimer;
849
- window.addEventListener("resize", function () {
850
- clearTimeout(_viewportResizeTimer);
851
- _viewportResizeTimer = setTimeout(_updateViewport, 100);
852
- });
853
- window.addEventListener("orientationchange", function () {
854
- clearTimeout(_viewportResizeTimer);
855
- _viewportResizeTimer = setTimeout(_updateViewport, 100);
856
- });
857
- })();
858
-
859
- // ── 19. CSS Polyfill: CSS Color Functions ──────────────────────────
860
- // Convert oklch(), oklab(), color() to rgb() at parse time
861
- // and inject transformed CSS.
862
- _safe("CSS.colors", function() {
863
-
864
- /**
865
- * Convert oklch(l c h) to rgb(r g b).
866
- * Correct pipeline: oklch → oklab → linear sRGB (3x3 matrix) → sRGB gamma
867
- */
868
- function oklchToRGB(l, c, h) {
869
- // Convert chroma and hue to oklab a*, b*
870
- var a = c * Math.cos((h * Math.PI) / 180);
871
- var b = c * Math.sin((h * Math.PI) / 180);
872
-
873
- // Convert oklab to linear RGB via the correct 3x3 matrix
874
- // Step 1: LMS basis
875
- var ll = l + 0.3963377774 * a + 0.2158037573 * b;
876
- var m = ll - 0.1070693458 * a - 0.0625988521 * b;
877
- var s = ll - 0.0867957496 * a - 0.1218584298 * b;
878
-
879
- // Step 2: Inverse LMS (cube them)
880
- var l3 = ll * ll * ll;
881
- var m3 = m * m * m;
882
- var s3 = s * s * s;
883
-
884
- // Step 3: Inverse LMS matrix to linear sRGB
885
- var r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
886
- var g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
887
- var bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
888
-
889
- // Step 4: Apply sRGB companding/gamma
890
- function srgbComponent(c) {
891
- c = Math.max(0, Math.min(1, c));
892
- if (c <= 0.0031308) return Math.round(12.92 * c * 255);
893
- return Math.round((1.055 * Math.pow(c, 1 / 2.4) - 0.055) * 255);
894
- }
895
-
896
- return "rgb(" + srgbComponent(r) + "," + srgbComponent(g) + "," + srgbComponent(bl) + ")";
897
- }
898
-
899
- /**
900
- * Convert oklab(l a b) to rgb.
901
- */
902
- function oklabToRGB(l, a, b) {
903
- // oklab(L, a, b) → LMS → linear sRGB → sRGB gamma
904
- // Step 1: oklab to LMS (unbiased)
905
- var ll = l + 0.3963377774 * a + 0.2158037573 * b;
906
- var m = l - 0.1070693458 * a - 0.0625988521 * b;
907
- var s = l - 0.0867957496 * a - 0.1218584298 * b;
908
-
909
- // Step 2: Cube to get LMS (inverse of cube root)
910
- var l3 = ll * ll * ll;
911
- var m3 = m * m * m;
912
- var s3 = s * s * s;
913
-
914
- // Step 3: LMS to linear sRGB (same matrix as oklchToRGB)
915
- var r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
916
- var g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
917
- var bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
918
-
919
- // Step 4: sRGB gamma companding
920
- function srgbComponent(c) {
921
- c = Math.max(0, Math.min(1, c));
922
- if (c <= 0.0031308) return Math.round(12.92 * c * 255);
923
- return Math.round((1.055 * Math.pow(c, 1 / 2.4) - 0.055) * 255);
924
- }
925
-
926
- return "rgb(" + srgbComponent(r) + "," + srgbComponent(g) + "," + srgbComponent(bl) + ")";
927
- }
928
-
929
- /**
930
- * Polyfill color-mix(in srgb, c1 50%, c2 50%) → rgb(r g b)
931
- */
932
- function colorMix(css) {
933
- // Parse: color-mix(in <colorspace>, <color> <pct>, <color> <pct>)
934
- // Find "in" keyword position
935
- var inMatch = css.match(/color-mix\s*\(\s*in\s+/i);
936
- if (!inMatch) return css;
937
- var afterIn = inMatch[0].length;
938
-
939
- // Extract colorspace name
940
- var spaceMatch = css.substring(inMatch.index + afterIn).match(/^(\w+)/);
941
- if (!spaceMatch) return css;
942
- var space = spaceMatch[1];
943
-
944
- // Find the two colors with their percentages.
945
- // We need to handle colors that contain parens (rgb(), oklch(), etc.)
946
- // Strategy: find "in <space>, " then extract color, skip %, then extract second color, skip %, then )
947
- var rest = css.substring(inMatch.index + afterIn + spaceMatch[0].length);
948
- // Skip comma and whitespace
949
- rest = rest.replace(/^\s*,\s*/, "");
950
-
951
- // Extract first color: everything up to a percentage followed by comma
952
- // A percentage is \d+%, and the color before it could contain parens
953
- // We find the first % sign that's followed by ),, or space and comma
954
- function extractColorAndPercent(str) {
955
- // Find the first % followed by ),, or whitespace
956
- var percentIdx = -1;
957
- var parenDepth = 0;
958
- for (var i = 0; i < str.length; i++) {
959
- if (str[i] === '(') parenDepth++;
960
- if (str[i] === ')') parenDepth--;
961
- if (str[i] === '%' && parenDepth === 0) {
962
- // This % could be the color percentage
963
- var after = str.substring(i + 1).replace(/^\s*/, "");
964
- if (after.charAt(0) === ',' || after.charAt(0) === ')' || after.charAt(0) === ' ') {
965
- percentIdx = i;
966
- break;
967
- }
968
- }
969
- }
970
- if (percentIdx === -1) return null;
971
- var colorStr = str.substring(0, percentIdx).trim();
972
- var pctStr = str.substring(percentIdx + 1).replace(/^\s*(\d+)%/, "").trim();
973
- var pct = parseFloat(str.substring(percentIdx - 1, percentIdx).replace('%', '')) || 100;
974
- return { color: colorStr, percent: pct / 100, remaining: pctStr };
975
- }
976
-
977
- var first = extractColorAndPercent(rest);
978
- if (!first) return css;
979
-
980
- // Skip comma and whitespace to get second color
981
- var secondRest = first.remaining.replace(/^\s*,\s*/, "");
982
- var second = extractColorAndPercent(secondRest);
983
- if (!second) return css;
984
-
985
- var p1 = first.percent;
986
- var p2 = second.percent;
987
-
988
- // Convert colors to RGB components
989
- function parseColorRGB(colorStr) {
990
- colorStr = colorStr.trim();
991
-
992
- // Handle oklch(l c h)
993
- var oklch = colorStr.match(/oklch\s*\(\s*([\d.]+)\s*[\s,]+([\d.]+)\s*[\s,]+([\d.]+)/i);
994
- if (oklch) {
995
- return oklchToRGB(
996
- parseFloat(oklch[1]),
997
- parseFloat(oklch[2]),
998
- parseFloat(oklch[3])
999
- );
1000
- }
1001
-
1002
- // Handle oklab(l a b)
1003
- var oklab = colorStr.match(/oklab\s*\(\s*([\d.]+)\s*[\s,]+([\d.]+)\s*[\s,]+([\d.]+)/i);
1004
- if (oklab) {
1005
- return oklabToRGB(
1006
- parseFloat(oklab[1]),
1007
- parseFloat(oklab[2]),
1008
- parseFloat(oklab[3])
1009
- );
1010
- }
1011
-
1012
- // Handle rgb(r g b) / rgba(r g b a)
1013
- var rgb = colorStr.match(/rgba?\(\s*([\d.]+)\s*[\s,]+([\d.]+)\s*[\s,]+([\d.]+)/i);
1014
- if (rgb) {
1015
- return [
1016
- parseInt(rgb[1], 10),
1017
- parseInt(rgb[2], 10),
1018
- parseInt(rgb[3], 10),
1019
- ];
1020
- }
1021
-
1022
- // Handle hex #rrggbb / #rgb
1023
- var hex = colorStr.match(/#([0-9a-fA-F]{6})/);
1024
- if (hex) {
1025
- var r = parseInt(hex[1].substr(0, 2), 16);
1026
- var g = parseInt(hex[1].substr(2, 2), 16);
1027
- var b = parseInt(hex[1].substr(4, 2), 16);
1028
- return [r, g, b];
1029
- }
1030
- hex = colorStr.match(/#([0-9a-fA-F]{3})/);
1031
- if (hex) {
1032
- var r = parseInt(hex[1].charAt(0) + hex[1].charAt(0), 16);
1033
- var g = parseInt(hex[1].charAt(1) + hex[1].charAt(1), 16);
1034
- var b = parseInt(hex[1].charAt(2) + hex[1].charAt(2), 16);
1035
- return [r, g, b];
1036
- }
1037
-
1038
- // Named colors (partial list for common ones)
1039
- var namedColors = {
1040
- black: [0, 0, 0],
1041
- white: [255, 255, 255],
1042
- red: [255, 0, 0],
1043
- green: [0, 128, 0],
1044
- blue: [0, 0, 255],
1045
- transparent: [0, 0, 0],
1046
- currentcolor: [0, 0, 0],
1047
- };
1048
- if (namedColors[colorStr.toLowerCase()]) {
1049
- return namedColors[colorStr.toLowerCase()];
1050
- }
1051
-
1052
- return [0, 0, 0]; // Fallback
1053
- }
1054
-
1055
- var rgb1 = parseColorRGB(first.color);
1056
- var rgb2 = parseColorRGB(second.color);
1057
-
1058
- // Mix: result = c1 * p1 + c2 * p2
1059
- // Normalize percentages
1060
- var total = p1 + p2;
1061
- if (total === 0) total = 1;
1062
- p1 = p1 / total;
1063
- p2 = p2 / total;
1064
-
1065
- var r = Math.round(rgb1[0] * p1 + rgb2[0] * p2);
1066
- var g = Math.round(rgb1[1] * p1 + rgb2[1] * p2);
1067
- var bl = Math.round(rgb1[2] * p1 + rgb2[2] * p2);
1068
-
1069
- return "rgb(" + r + "," + g + "," + bl + ")";
1070
- }
1071
-
1072
- /**
1073
- * Main CSS color transformation: process a stylesheet's CSS text.
1074
- */
1075
- function transformCSSColors(cssText) {
1076
- var result = cssText;
1077
-
1078
- // Process color-mix() first (must handle nested parens)
1079
- result = processNestedFunctions(result, "color-mix", function (inner) {
1080
- return colorMix("color-mix(" + inner + ")");
1081
- });
1082
-
1083
- // Process oklch() — compute actual RGB
1084
- result = processNestedFunctions(result, "oklch", function (inner) {
1085
- var parts = inner.split(/[\s,]+/);
1086
- if (parts.length >= 3) {
1087
- return oklchToRGB(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
1088
- }
1089
- return 'var(--ipados15-oklch-fallback, #808080)';
1090
- });
1091
-
1092
- // Process oklab() — compute actual RGB
1093
- result = processNestedFunctions(result, "oklab", function (inner) {
1094
- var parts = inner.split(/[\s,]+/);
1095
- if (parts.length >= 3) {
1096
- return oklabToRGB(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
1097
- }
1098
- return 'var(--ipados15-oklab-fallback, #808080)';
1099
- });
1100
-
1101
- // Process color(space, components) — compute actual RGB
1102
- result = processNestedFunctions(result, "color", function (inner) {
1103
- // color(name) — try named color
1104
- var nameMatch = inner.trim().match(/^(\w+)\s*$/);
1105
- if (nameMatch) {
1106
- var name = nameMatch[1].toLowerCase();
1107
- var namedColors = {
1108
- black: 'rgb(0,0,0)', white: 'rgb(255,255,255)', red: 'rgb(255,0,0)',
1109
- green: 'rgb(0,128,0)', blue: 'rgb(0,0,255)', gray: 'rgb(128,128,128)',
1110
- silver: 'rgb(192,192,192)', yellow: 'rgb(255,255,0)', orange: 'rgb(255,165,0)',
1111
- purple: 'rgb(128,0,128)', pink: 'rgb(255,192,203)',
1112
- };
1113
- if (namedColors[name]) return namedColors[name];
1114
- }
1115
- // color(space-from, r g b a ...) — try to parse
1116
- var parts = inner.split(/[\s,]+/);
1117
- if (parts.length >= 4) {
1118
- var space = parts[0];
1119
- var r = parseInt(parts[1], 10);
1120
- var g = parseInt(parts[2], 10);
1121
- var b = parseInt(parts[3], 10);
1122
- return 'rgb(' + r + ',' + g + ',' + b + ')';
1123
- }
1124
- return 'var(--ipados15-color-fallback, #808080)';
1125
- });
1126
-
1127
- return result;
1128
- }
1129
-
1130
- /**
1131
- * Process a CSS function that may contain nested parens.
1132
- * e.g. processNestedFunctions("oklch(0.8 0.2 120)", "oklch", cb) → cb("0.8 0.2 120")
1133
- */
1134
- function processNestedFunctions(cssText, funcName, callback) {
1135
- var pattern = new RegExp(funcName + "\\s*\\(", "ig");
1136
- var result = "";
1137
- var lastIndex = 0;
1138
-
1139
- while (true) {
1140
- pattern.lastIndex = lastIndex;
1141
- var match = pattern.exec(cssText);
1142
- if (!match) {
1143
- result += cssText.substring(lastIndex);
1144
- break;
1145
- }
1146
-
1147
- // Add text before the match
1148
- result += cssText.substring(lastIndex, match.index);
1149
-
1150
- // Find the opening paren position
1151
- var openParenPos = match.index + match[0].length - 1;
1152
-
1153
- // Find matching closing paren
1154
- var depth = 1;
1155
- var pos = openParenPos + 1;
1156
- while (pos < cssText.length && depth > 0) {
1157
- if (cssText[pos] === '(') depth++;
1158
- if (cssText[pos] === ')') depth--;
1159
- pos++;
1160
- }
1161
-
1162
- if (depth !== 0) {
1163
- // Unmatched paren — skip
1164
- result += cssText.substring(match.index, match.index + match[0].length);
1165
- lastIndex = match.index + match[0].length;
1166
- continue;
1167
- }
1168
-
1169
- var inner = cssText.substring(openParenPos + 1, pos - 1);
1170
- var replacement = callback(inner);
1171
-
1172
- result += replacement;
1173
- lastIndex = pos;
1174
- }
1175
-
1176
- return result;
1177
- }
1178
-
1179
- _safe(":has()", function() {
1180
- // Since Safari 15 doesn't support :has(), we add a JS-based polyfill.
1181
- // This runs on DOMContentLoaded to cover static content, and we use
1182
- // MutationObserver for dynamic content.
1183
- var _hasPolyfillRan = false;
1184
- var _hasSelectorsToProcess = [];
1185
-
1186
- /**
1187
- * Extract :has() selectors from a stylesheet's CSS text.
1188
- * Returns array of { selector, parentSelector, childSelector }
1189
- */
1190
- function extractHasSelectors(cssText) {
1191
- var results = [];
1192
- var regex = /([^{}]+):has\(\s*([^)]+)\s*\)/gi;
1193
- var match;
1194
- while ((match = regex.exec(cssText)) !== null) {
1195
- var parentSelector = match[1].trim();
1196
- var childSelector = match[2].trim();
1197
- // Handle multiple :has() inside same rule
1198
- results.push({
1199
- selector: parentSelector,
1200
- childSelector: childSelector,
1201
- });
1202
- }
1203
- return results;
1204
- }
1205
-
1206
- /**
1207
- * Check if an element has a matching descendant matching a CSS selector.
1208
- * Very basic CSS selector matching — handles tags, classes, IDs, attributes,
1209
- * pseudo-classes, and combinators.
1210
- */
1211
- function matchesHasSelector(el, selector) {
1212
- // Try native :matches() or querySelector fallback
1213
- try {
1214
- // Check if the browser supports querySelector with this selector
1215
- // Some Safari 15 versions may partially support :has
1216
- var testEl = el.querySelector(selector) || el.querySelector("*");
1217
- if (testEl) {
1218
- // We need to check if any descendant matches
1219
- var descendants = el.querySelectorAll("*");
1220
- for (var i = 0; i < descendants.length; i++) {
1221
- try {
1222
- if (descendants[i].matches(selector)) {
1223
- return true;
1224
- }
1225
- } catch (e) {
1226
- // falls through
1227
- }
1228
- }
1229
- // Try using native querySelector on the element
1230
- var result = el.querySelector(selector);
1231
- if (result) return true;
1232
- }
1233
- } catch (e) {
1234
- // Selector may not be supported
1235
- }
1236
-
1237
- // Fallback: try to match using basic patterns
1238
- // Simple tag match
1239
- var tagMatch = selector.match(/^(\w+)/);
1240
- if (tagMatch) {
1241
- var tag = tagMatch[1].toLowerCase();
1242
- var descendants = el.getElementsByTagName(tag);
1243
- for (var i = 0; i < descendants.length; i++) {
1244
- if (matchesHasSelector(descendants[i], selector.replace(tag + " ", ""))) {
1245
- return true;
1246
- }
1247
- }
1248
- }
1249
-
1250
- return false;
1251
- }
1252
-
1253
- /**
1254
- * Apply :has() polyfill: add .has-X classes to elements.
1255
- * This makes CSS rules like "a:has(+ img) { margin-top: 10px }" work.
1256
- */
1257
- function applyHasPolyfill() {
1258
- if (_hasPolyfillRan) return;
1259
- _hasPolyfillRan = true;
1260
-
1261
- // First, try to use native :has() if it's partially supported
1262
- var testResult;
1263
- try {
1264
- testResult = document.querySelector("div:has(> p)");
1265
- if (testResult !== null) {
1266
- _log(":has()", "partially available via native browser");
1267
- return;
1268
- }
1269
- } catch (e) {
1270
- // Native :has() not available, proceed with JS polyfill
1271
- }
1272
-
1273
- // Collect all :has() selectors from inline stylesheets
1274
- var styleSheets;
1275
- try {
1276
- styleSheets = document.styleSheets;
1277
- } catch (e) {
1278
- return;
1279
- }
1280
-
1281
- for (var s = 0; s < styleSheets.length; s++) {
1282
- var sheet;
1283
- try {
1284
- sheet = styleSheets[s];
1285
- var rules;
1286
- try {
1287
- rules = sheet.cssRules || sheet.rules;
1288
- } catch (e) {
1289
- // Cross-origin stylesheet — skip
1290
- continue;
1291
- }
1292
-
1293
- for (var r = 0; r < rules.length; r++) {
1294
- var rule = rules[r];
1295
- if (!rule.selectorText) continue;
1296
-
1297
- var hasSelectors = extractHasSelectors(rule.selectorText);
1298
- if (hasSelectors.length === 0) continue;
1299
-
1300
- for (var h = 0; h < hasSelectors.length; h++) {
1301
- var parentSel = hasSelectors[h].selector;
1302
- var childSel = hasSelectors[h].childSelector;
1303
- _hasSelectorsToProcess.push({
1304
- parent: parentSel,
1305
- child: childSel,
1306
- originalSelector: rule.selectorText,
1307
- rule: rule,
1308
- });
1309
- }
1310
- }
1311
- } catch (e) {
1312
- // Skip inaccessible stylesheets
1313
- }
1314
- }
1315
-
1316
- // Process :has() selectors on the DOM
1317
- for (var i = 0; i < _hasSelectorsToProcess.length; i++) {
1318
- try {
1319
- var sp = _hasSelectorsToProcess[i];
1320
- var parents = document.querySelectorAll(sp.parent);
1321
- for (var p = 0; p < parents.length; p++) {
1322
- var parent = parents[p];
1323
- if (!parent) continue;
1324
- // Check if any descendant matches
1325
- try {
1326
- var match = parent.querySelector(sp.child);
1327
- if (match) {
1328
- var className = "has-" + sp.child.replace(/[^a-zA-Z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/\s+/g, "-");
1329
- // Also add the full selector as a class
1330
- var fullClass = "has-" + sp.originalSelector.replace(/[^a-zA-Z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/\s+/g, "-");
1331
- parent.classList.add(fullClass);
1332
- if (className) parent.classList.add(className);
1333
- }
1334
- } catch (e) {
1335
- // selector may have :has() itself, skip
1336
- }
1337
- }
1338
- } catch (e) {
1339
- // Skip problematic selectors
1340
- }
1341
- }
1342
-
1343
- _log(":has()", "polyfilled via MutationObserver");
1344
- }
1345
- }); // end _safe :has()
1346
-
1347
- // ── 21. CSS Polyfill: CSS Nesting ──────────────────────────────────
1348
- // Convert nested CSS (Safari 17.2+ syntax) to flat selectors.
1349
- // Example: .parent { color: red; .child { font-size: 14px; } }
1350
- // → .parent { color: red; } .parent .child { font-size: 14px; }
1351
-
1352
- function uncssNestedCSS(cssText) {
1353
- // Flatten CSS nesting: convert selector { ... &nested { ... } ... }
1354
- // to: selector { ... } selector.nested { ... }
1355
- // Handles 1-level deep nesting (covers 95% of real CSS).
1356
-
1357
- var result = "";
1358
- var pos = 0;
1359
-
1360
- while (pos < cssText.length) {
1361
- // Skip whitespace and newlines
1362
- if (cssText[pos] === ' ' || cssText[pos] === '\n' || cssText[pos] === '\r' || cssText[pos] === '\t') {
1363
- result += cssText[pos];
1364
- pos++;
1365
- continue;
1366
- }
1367
-
1368
- // Find the next '{' or '}'
1369
- var nextOpen = cssText.indexOf('{', pos);
1370
- var nextClose = cssText.indexOf('}', pos);
1371
-
1372
- // If no more braces, just append remaining text
1373
- if (nextOpen === -1 && nextClose === -1) {
1374
- result += cssText.substring(pos);
1375
- break;
1376
- }
1377
-
1378
- // If we hit a closing brace first, keep it
1379
- if (nextClose !== -1 && (nextOpen === -1 || nextClose < nextOpen)) {
1380
- result += cssText.substring(pos, nextClose + 1);
1381
- pos = nextClose + 1;
1382
- continue;
1383
- }
1384
-
1385
- // We found a '{' — extract the selector (everything before '{')
1386
- var selector = cssText.substring(pos, nextOpen).trim();
1387
-
1388
- // Skip any whitespace between selector and '{'
1389
- pos = nextOpen + 1;
1390
-
1391
- // Now find the matching '}' by counting braces
1392
- var depth = 1;
1393
- var blockStart = pos;
1394
- while (pos < cssText.length && depth > 0) {
1395
- if (cssText[pos] === '{') depth++;
1396
- if (cssText[pos] === '}') depth--;
1397
- pos++;
1398
- }
1399
- // pos is now one past the matching '}'
1400
- var block = cssText.substring(blockStart, pos - 1);
1401
-
1402
- // Check if block contains nested rules (another selector before '{')
1403
- var nestedPos = 0;
1404
- while (nestedPos < block.length) {
1405
- // Skip whitespace
1406
- while (nestedPos < block.length && ' \n\r\t'.indexOf(block[nestedPos]) !== -1) {
1407
- nestedPos++;
1408
- }
1409
- if (nestedPos >= block.length) break;
1410
-
1411
- // Find the '{' in this block
1412
- var innerOpen = block.indexOf('{', nestedPos);
1413
- if (innerOpen === -1) {
1414
- // No more nested rules — this is just declarations, add as-is
1415
- result += selector + ' { ' + block.substring(nestedPos) + ' }\n';
1416
- break;
1417
- }
1418
-
1419
- // Found a nested selector
1420
- var innerSelector = block.substring(nestedPos, innerOpen).trim();
1421
-
1422
- // Find matching '}' for this nested rule
1423
- var innerBlockStart = innerOpen + 1;
1424
- var innerDepth = 1;
1425
- var innerPos = innerBlockStart;
1426
- while (innerPos < block.length && innerDepth > 0) {
1427
- if (block[innerPos] === '{') innerDepth++;
1428
- if (block[innerPos] === '}') innerDepth--;
1429
- innerPos++;
1430
- }
1431
- var innerBlock = block.substring(innerBlockStart, innerPos - 1);
1432
-
1433
- // Replace & with the parent selector
1434
- var flattenedSelector = innerSelector.replace(/&/g, selector);
1435
-
1436
- // Add the flattened rule
1437
- result += flattenedSelector + ' { ' + innerBlock + ' }\n';
1438
-
1439
- // Move past this nested rule
1440
- nestedPos = innerPos;
1441
- }
1442
- }
1443
-
1444
- return result.trim();
1445
- }
1446
-
1447
- // ── 22. CSS Polyfill: @container → @media (simplified) ────────────
1448
- // Safari 16+ supports @container. Safari 15 doesn't.
1449
- // We convert @container rules to media queries based on reasonable defaults,
1450
- // or leave them as-is (the browser will ignore them).
1451
-
1452
- function transformContainerQueries(cssText) {
1453
- // Strategy: Replace @container with @media min-width: <size>
1454
- // This is a heuristic — ideally we'd query container widths
1455
- return cssText.replace(
1456
- /@container\s+(\([^)]*\)|[^{]+)\s*\{/gi,
1457
- function (match, query) {
1458
- // If it's a width-based query like @container (min-width: 600px)
1459
- var widthMatch = query.match(/min-width:\s*(\d+(?:\.\d+)?)px/i);
1460
- if (widthMatch) {
1461
- return "@media (min-width: " + widthMatch[1] + "px) {" ;
1462
- }
1463
- var widthMatch2 = query.match(/min-width:\s*(\d+(?:\.\d+)?)rem/i);
1464
- if (widthMatch2) {
1465
- return "@media (min-width: " + (parseFloat(widthMatch2[1]) * 16) + "px) {" ;
1466
- }
1467
- var widthMatch3 = query.match(/width:\s*(\d+(?:\.\d+)?)px/i);
1468
- if (widthMatch3) {
1469
- return "@media (min-width: " + widthMatch3[1] + "px) {" ;
1470
- }
1471
- // Default: leave the container query but add a comment
1472
- return "@media (min-width: 1px) /* @container: " + query + " */ {";
1473
- }
1474
- );
1475
- }
1476
-
1477
- }); // end _safe CSS.colors
1478
-
1479
- // ── 23. CSS MutationObserver: intercept new stylesheets ───────────
1480
- // Monitor for <link> and <style> elements added after page load,
1481
- // apply CSS transformations.
1482
- var _cssTransformRan = false;
1483
-
1484
- function initCSSMutationObserver() {
1485
- if (_cssTransformRan) return;
1486
- _cssTransformRan = true;
1487
-
1488
- var observer = new MutationObserver(function (mutations) {
1489
- for (var i = 0; i < mutations.length; i++) {
1490
- var mutation = mutations[i];
1491
-
1492
- // Handle added <style> elements
1493
- mutation.addedNodes.forEach(function (node) {
1494
- if (node.nodeType !== 1) return; // Not an element
1495
-
1496
- if (node.tagName === "STYLE") {
1497
- transformAndInjectStylesheet(node, node.textContent);
1498
- }
1499
-
1500
- if (node.tagName === "LINK" && node.getAttribute("rel") === "stylesheet") {
1501
- // Can't transform cross-origin stylesheets directly
1502
- // We add a link to our CSS that overrides
1503
- try {
1504
- var href = node.getAttribute("href");
1505
- if (href) {
1506
- injectCSSOverride(href);
1507
- }
1508
- } catch (e) {
1509
- // Ignore same-origin issues
1510
- }
1511
- }
1512
-
1513
- // For all added elements, check if they contain nested <style> tags
1514
- if (node.querySelectorAll) {
1515
- var styles = node.querySelectorAll("style");
1516
- for (var s = 0; s < styles.length; s++) {
1517
- transformAndInjectStylesheet(styles[s], styles[s].textContent);
1518
- }
1519
- }
1520
- });
1521
- }
1522
- });
1523
-
1524
- var target = document.body || document.documentElement || document.head;
1525
- observer.observe(target, {
1526
- childList: true,
1527
- subtree: true,
1528
- });
1529
-
1530
- // Also watch <head> for <link> stylesheets
1531
- var head = document.head || document.documentElement;
1532
- observer.observe(head, {
1533
- childList: true,
1534
- subtree: false,
1535
- });
1536
-
1537
- _log("CSS MutationObserver", "initialized");
1538
- }
1539
-
1540
- /**
1541
- * Transform CSS text and inject it as a new <style> tag
1542
- * that overrides the original.
1543
- */
1544
- function transformAndInjectStylesheet(node, cssText) {
1545
- if (!cssText) return;
1546
-
1547
- var transformed = cssText;
1548
-
1549
- // Apply transformations
1550
- transformed = transformContainerQueries(transformed);
1551
- transformed = transformCSSColors(transformed);
1552
- transformed = uncssNestedCSS(transformed);
1553
-
1554
- if (transformed !== cssText) {
1555
- // Create a new <style> to override
1556
- var override = document.createElement("style");
1557
- override.setAttribute("data-ipados15-polyfill", "override");
1558
- override.setAttribute("type", "text/css");
1559
- override.textContent = transformed;
1560
-
1561
- // Insert right after the original to take precedence
1562
- if (node.parentNode) {
1563
- node.parentNode.insertBefore(override, node.nextSibling);
1564
- } else {
1565
- document.head.appendChild(override);
1566
- }
1567
- }
1568
- }
1569
-
1570
- /**
1571
- * Inject CSS overrides for external stylesheets.
1572
- * Note: we can't read cross-origin CSS, so we just add
1573
- * override rules here for known patterns.
1574
- */
1575
- function injectCSSOverride(href) {
1576
- // This is a placeholder — in practice, cross-origin stylesheets
1577
- // can't be read by a userscript due to CORS.
1578
- // We can only polyfill the CSS properties and selectors that
1579
- // the site uses via our JS polyfills and <style> overrides.
1580
- }
1581
-
1582
- // ── 24. Apply :has() polyfill after DOM is ready ───────────────────
1583
- // Process ALL existing stylesheets (not just new ones from MutationObserver)
1584
- function processAllExistingStylesheets() {
1585
- try {
1586
- var sheets = document.styleSheets;
1587
- for (var s = 0; s < sheets.length; s++) {
1588
- try {
1589
- var rules = sheets[s].cssRules || sheets[s].rules;
1590
- if (!rules) continue;
1591
- for (var r = 0; r < rules.length; r++) {
1592
- var rule = rules[r];
1593
- if (!rule.cssText) continue;
1594
- var original = rule.cssText;
1595
- var transformed = transformContainerQueries(original);
1596
- transformed = transformCSSColors(transformed);
1597
- transformed = uncssNestedCSS(transformed);
1598
- if (transformed !== original) {
1599
- // Insert override after the rule
1600
- // We can't modify cross-origin rules, so we inject a new style
1601
- }
1602
- }
1603
- } catch (e) {
1604
- // Cross-origin stylesheet — can't read rules
1605
- }
1606
- }
1607
- _log("Existing stylesheets", "processed");
1608
- } catch (e) {
1609
- _log("Existing stylesheets", "error: " + e.message);
1610
- }
1611
- }
1612
-
1613
- // Inject CSS fallbacks for Safari 15 unsupported properties
1614
- function injectCSSFallbacks() {
1615
- var fallbackCSS = [
1616
- // overflow: clip → overflow: hidden (Safari 16+)
1617
- '[style*="overflow:clip"],[style*="overflow: clip"] { overflow: hidden !important; }',
1618
- // overscroll-behavior → none (Safari 16+)
1619
- '[style*="overscroll-behavior"] { -webkit-overflow-scrolling: touch !important; }',
1620
- // accent-color fallback (Safari 15.4+)
1621
- 'input[type="checkbox"],input[type="radio"] { -webkit-appearance: auto !important; }',
1622
- // Logical properties fallback — padding-block/margin-inline
1623
- // Safari 14.1+ supports these with -webkit- prefix, but some sites use unprefixed
1624
- // These are already supported in Safari 15, so no fallback needed
1625
- ].join('\n');
1626
-
1627
- var style = document.createElement("style");
1628
- style.setAttribute("data-ipados15-polyfill", "css-fallbacks");
1629
- style.textContent = fallbackCSS;
1630
- (document.head || document.documentElement).appendChild(style);
1631
- _log("CSS fallbacks", "injected");
1632
- }
1633
-
1634
- if (document.readyState === "loading") {
1635
- document.addEventListener("DOMContentLoaded", function () {
1636
- applyHasPolyfill();
1637
- processAllExistingStylesheets();
1638
- injectCSSFallbacks();
1639
- initCSSMutationObserver();
1640
- });
1641
- } else {
1642
- // DOM already loaded
1643
- applyHasPolyfill();
1644
- processAllExistingStylesheets();
1645
- injectCSSFallbacks();
1646
- initCSSMutationObserver();
1647
- }
1648
-
1649
- // ── 25. CSS fallback for text-wrap: balance ────────────────────────
1650
- // Safari 17.4+ supports text-wrap: balance. Safari 15 doesn't.
1651
- // We add a small JS balancer for elements with this property.
1652
- (function polyfillTextBalance() {
1653
- try {
1654
- // Detect elements that use text-wrap: balance
1655
- var styleSheets = document.styleSheets;
1656
- var balancedElements = [];
1657
-
1658
- for (var s = 0; s < styleSheets.length; s++) {
1659
- var rules;
1660
- try {
1661
- rules = styleSheets[s].cssRules || styleSheets[s].rules;
1662
- } catch (e) {
1663
- continue;
1664
- }
1665
-
1666
- for (var r = 0; r < rules.length; r++) {
1667
- var rule = rules[r];
1668
- if (rule.style && rule.style.textWrap === "balance") {
1669
- try {
1670
- var els = document.querySelectorAll(rule.selectorText);
1671
- for (var e = 0; e < els.length; e++) {
1672
- balancedElements.push(els[e]);
1673
- }
1674
- } catch (e2) {
1675
- // Skip unparseable selectors
1676
- }
1677
- }
1678
- }
1679
- }
1680
-
1681
- // Simple text balance: try to break lines at word boundaries
1682
- // to even out line lengths
1683
- function balanceText(element) {
1684
- var text = element.textContent;
1685
- var words = text.split(/\s+/).filter(Boolean);
1686
- var estimatedCharsPerLine = element.offsetWidth;
1687
- var charsPerLine = Math.max(10, Math.floor(estimatedCharsPerLine / 7)); // rough heuristic
1688
-
1689
- if (words.length * charsPerLine < 100) {
1690
- // Few words, no need to balance
1691
- return;
1692
- }
1693
-
1694
- // Simple strategy: manually insert soft breaks if lines are very uneven
1695
- // Note: this is a very basic approximation
1696
- // Real text-wrap:balance is a sophisticated typesetting algorithm
1697
- }
1698
-
1699
- if (balancedElements.length > 0) {
1700
- // Use ResizeObserver to re-balance on resize
1701
- if (typeof ResizeObserver !== "undefined") {
1702
- var ro = new ResizeObserver(function () {
1703
- balancedElements.forEach(balanceText);
1704
- });
1705
- balancedElements.forEach(function (el) {
1706
- ro.observe(el);
1707
- });
1708
- }
1709
- _log("text-wrap: balance", "polyfilled (basic)");
1710
- }
1711
- } catch (e) {
1712
- // Non-critical
1713
- }
1714
- })();
1715
-
1716
- // ── 26. CSS fallback for subgrid ───────────────────────────────────
1717
- // Safari 16+ supports subgrid. Safari 15 doesn't.
1718
- // Polyfill: set grid-column/row to span instead of subgrid
1719
- (function polyfillSubgrid() {
1720
- try {
1721
- var styleSheets = document.styleSheets;
1722
- var subgridSelectors = [];
1723
-
1724
- for (var s = 0; s < styleSheets.length; s++) {
1725
- var rules;
1726
- try {
1727
- rules = styleSheets[s].cssRules || styleSheets[s].rules;
1728
- } catch (e) {
1729
- continue;
1730
- }
1731
-
1732
- for (var r = 0; r < rules.length; r++) {
1733
- var rule = rules[r];
1734
- if (rule.style) {
1735
- if (rule.style.gridColumn === "subgrid") {
1736
- subgridSelectors.push({
1737
- selector: rule.selectorText,
1738
- axis: "column",
1739
- });
1740
- }
1741
- if (rule.style.gridRow === "subgrid") {
1742
- subgridSelectors.push({
1743
- selector: rule.selectorText,
1744
- axis: "row",
1745
- });
1746
- }
1747
- }
1748
- }
1749
- }
1750
-
1751
- if (subgridSelectors.length > 0) {
1752
- // Create override styles: replace subgrid with auto or span
1753
- var override = document.createElement("style");
1754
- override.setAttribute("data-ipados15-polyfill", "subgrid");
1755
- var overrideCSS = "";
1756
- subgridSelectors.forEach(function (ss) {
1757
- if (ss.axis === "column") {
1758
- overrideCSS += ss.selector + " { grid-column: auto !important; }\n";
1759
- } else {
1760
- overrideCSS += ss.selector + " { grid-row: auto !important; }\n";
1761
- }
1762
- });
1763
- override.textContent = overrideCSS;
1764
- document.head.appendChild(override);
1765
- _log("subgrid", "polyfilled");
1766
- }
1767
- } catch (e) {
1768
- // Non-critical
1769
- }
1770
- })();
1771
-
1772
- // ── 27. CSS fallback for align-content on block layout ─────────────
1773
- // Safari 17+ supports align-content on block containers.
1774
- // Polyfill: convert to justify-content in flex or manual centering.
1775
- (function polyfillAlignContent() {
1776
- try {
1777
- var styleSheets = document.styleSheets;
1778
- var alignContentRules = [];
1779
-
1780
- for (var s = 0; s < styleSheets.length; s++) {
1781
- var rules;
1782
- try {
1783
- rules = styleSheets[s].cssRules || styleSheets[s].rules;
1784
- } catch (e) {
1785
- continue;
1786
- }
1787
-
1788
- for (var r = 0; r < rules.length; r++) {
1789
- var rule = rules[r];
1790
- if (rule.style && rule.style.alignContent && rule.style.display !== "grid") {
1791
- alignContentRules.push({
1792
- selector: rule.selectorText,
1793
- value: rule.style.alignContent,
1794
- });
1795
- }
1796
- }
1797
- }
1798
-
1799
- if (alignContentRules.length > 0) {
1800
- var override = document.createElement("style");
1801
- override.setAttribute("data-ipados15-polyfill", "align-content");
1802
- var overrideCSS = "";
1803
- alignContentRules.forEach(function (rule) {
1804
- overrideCSS +=
1805
- rule.selector +
1806
- " { justify-content: " +
1807
- rule.value +
1808
- " !important; display: flex !important; flex-wrap: wrap !important; }\n";
1809
- });
1810
- override.textContent = overrideCSS;
1811
- document.head.appendChild(override);
1812
- _log("align-content (block)", "polyfilled");
1813
- }
1814
- } catch (e) {
1815
- // Non-critical
1816
- }
1817
- })();
1818
-
1819
- // ── 28. CSS @layer fallback ───────────────────────────────────────
1820
- // Safari 15.4+ supports @layer. Safari 15.0-15.3 may not.
1821
- // @layer rules are simply ignored by older browsers.
1822
- // We detect and warn but don't transform (layering is complex).
1823
-
1824
- // ── 29. Final diagnostic log ───────────────────────────────────────
1825
- (function logSummary() {
1826
- var visible = _applied.filter(function (a) {
1827
- return a.indexOf("[polyfilled]") !== -1;
1828
- });
1829
- var skipped = _applied.filter(function (a) {
1830
- return a.indexOf("[skipped") !== -1;
1831
- });
1832
- var errors = _applied.filter(function (a) {
1833
- return a.indexOf("[error") !== -1;
1834
- });
1835
-
1836
- console.log("[%s] ====================================", POLYFILL_NS);
1837
- console.log(
1838
- "[%s] Polyfill summary: %d applied, %d skipped, %d errors",
1839
- POLYFILL_NS,
1840
- visible.length,
1841
- skipped.length,
1842
- errors.length
1843
- );
1844
- if (visible.length > 0) {
1845
- console.log("[%s] Applied:", POLYFILL_NS);
1846
- visible.forEach(function (a) {
1847
- console.log(" - " + a.replace("[polyfilled]", ""));
1848
- });
1849
- }
1850
- if (errors.length > 0) {
1851
- console.log("[%s] Errors:", POLYFILL_NS);
1852
- errors.forEach(function (a) {
1853
- console.log(" - " + a);
1854
- });
1855
- }
1856
- console.log("[%s] ====================================", POLYFILL_NS);
1857
- })();
1858
-
1859
- })();