@salve-software/react-native-nitro-jsdom 1.0.1 → 2.0.0
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.
- package/android/CMakeLists.txt +18 -0
- package/cpp/HybridHtmlSandbox.cpp +20 -3
- package/cpp/HybridHtmlSandbox.hpp +1 -1
- package/cpp/lexbor/LexborDocument.cpp +279 -67
- package/cpp/lexbor/LexborDocument.hpp +43 -10
- package/cpp/quickjs/DOMBindings.cpp +37 -1
- package/cpp/quickjs/DOMBindingsInternal.cpp +101 -2
- package/cpp/quickjs/DOMBindingsInternal.hpp +56 -1
- package/cpp/quickjs/QuickJSRuntime.cpp +32 -4
- package/cpp/quickjs/QuickJSRuntime.hpp +50 -1
- package/cpp/quickjs/bindings/AbortBindings.cpp +128 -0
- package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
- package/cpp/quickjs/bindings/BlobBindings.cpp +164 -0
- package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
- package/cpp/quickjs/bindings/CSSOMBindings.cpp +290 -0
- package/cpp/quickjs/bindings/CSSOMBindings.hpp +25 -0
- package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
- package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
- package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
- package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
- package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
- package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
- package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
- package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
- package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
- package/cpp/quickjs/bindings/DocumentBindings.cpp +239 -4
- package/cpp/quickjs/bindings/ElementBindings.cpp +556 -44
- package/cpp/quickjs/bindings/EventBindings.cpp +414 -8
- package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
- package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
- package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
- package/cpp/quickjs/bindings/FetchBindings.cpp +45 -6
- package/cpp/quickjs/bindings/FormBindings.cpp +365 -0
- package/cpp/quickjs/bindings/FormBindings.hpp +20 -0
- package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
- package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +228 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
- package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
- package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
- package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
- package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
- package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
- package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
- package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
- package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
- package/cpp/quickjs/bindings/TimerBindings.cpp +120 -0
- package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
- package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
- package/cpp/quickjs/bindings/UrlBindings.cpp +295 -0
- package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
- package/cpp/quickjs/bindings/WindowBindings.cpp +552 -0
- package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
- package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
- package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
- package/package.json +17 -2
- package/src/classes/JSDOM/JSDOM.class.ts +1 -0
- package/src/specs/HtmlSandbox.nitro.ts +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#include "BlobBindings.hpp"
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
namespace margelo::nitro::nitrojsdom {
|
|
5
|
+
|
|
6
|
+
namespace {
|
|
7
|
+
|
|
8
|
+
const char* kBlobBootstrapScript = R"JS(
|
|
9
|
+
(function() {
|
|
10
|
+
function partToBytes(part) {
|
|
11
|
+
if (part instanceof ArrayBuffer) {
|
|
12
|
+
return Array.prototype.slice.call(new Uint8Array(part));
|
|
13
|
+
}
|
|
14
|
+
if (part && typeof part.byteLength === 'number' && part.buffer !== undefined) {
|
|
15
|
+
return Array.prototype.slice.call(new Uint8Array(part.buffer, part.byteOffset, part.byteLength));
|
|
16
|
+
}
|
|
17
|
+
if (part instanceof Blob) {
|
|
18
|
+
return part._bytes.slice();
|
|
19
|
+
}
|
|
20
|
+
return Array.prototype.slice.call(new TextEncoder().encode(String(part)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeBlobType(type) {
|
|
24
|
+
if (type === undefined || type === null) return '';
|
|
25
|
+
var s = String(type);
|
|
26
|
+
for (var i = 0; i < s.length; i++) {
|
|
27
|
+
var code = s.charCodeAt(i);
|
|
28
|
+
if (code < 0x20 || code > 0x7e) return '';
|
|
29
|
+
}
|
|
30
|
+
return s.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Blob(parts, options) {
|
|
34
|
+
var bytes = [];
|
|
35
|
+
if (parts) {
|
|
36
|
+
for (var i = 0; i < parts.length; i++) {
|
|
37
|
+
bytes = bytes.concat(partToBytes(parts[i]));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this._bytes = bytes;
|
|
41
|
+
this.size = bytes.length;
|
|
42
|
+
this.type = normalizeBlobType(options && options.type);
|
|
43
|
+
}
|
|
44
|
+
Blob.prototype.slice = function(start, end, contentType) {
|
|
45
|
+
var sliced = this._bytes.slice(start, end);
|
|
46
|
+
var result = new Blob([]);
|
|
47
|
+
result._bytes = sliced;
|
|
48
|
+
result.size = sliced.length;
|
|
49
|
+
result.type = contentType ? String(contentType) : '';
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
Blob.prototype.text = function() {
|
|
53
|
+
var bytes = this._bytes;
|
|
54
|
+
var buf = new ArrayBuffer(bytes.length);
|
|
55
|
+
var view = new Uint8Array(buf);
|
|
56
|
+
for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
|
|
57
|
+
return Promise.resolve(new TextDecoder().decode(buf));
|
|
58
|
+
};
|
|
59
|
+
Blob.prototype.arrayBuffer = function() {
|
|
60
|
+
var bytes = this._bytes;
|
|
61
|
+
var buf = new ArrayBuffer(bytes.length);
|
|
62
|
+
var view = new Uint8Array(buf);
|
|
63
|
+
for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
|
|
64
|
+
return Promise.resolve(buf);
|
|
65
|
+
};
|
|
66
|
+
globalThis.Blob = Blob;
|
|
67
|
+
|
|
68
|
+
function File(parts, name, options) {
|
|
69
|
+
Blob.call(this, parts, options);
|
|
70
|
+
var lastModified;
|
|
71
|
+
if (options && options.lastModified !== undefined) {
|
|
72
|
+
var n = Number(options.lastModified);
|
|
73
|
+
lastModified = (isNaN(n) || !isFinite(n)) ? 0 : Math.trunc(n);
|
|
74
|
+
} else {
|
|
75
|
+
lastModified = Date.now();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Object.defineProperty(this, 'name', { value: String(name), enumerable: true, configurable: true });
|
|
79
|
+
Object.defineProperty(this, 'lastModified', { value: lastModified, enumerable: true, configurable: true });
|
|
80
|
+
}
|
|
81
|
+
File.prototype = Object.create(Blob.prototype);
|
|
82
|
+
File.prototype.constructor = File;
|
|
83
|
+
globalThis.File = File;
|
|
84
|
+
|
|
85
|
+
function FileReader() {
|
|
86
|
+
this.readyState = 0;
|
|
87
|
+
this.result = null;
|
|
88
|
+
this.error = null;
|
|
89
|
+
this.onload = null;
|
|
90
|
+
this.onloadend = null;
|
|
91
|
+
this.onerror = null;
|
|
92
|
+
this._listeners = {};
|
|
93
|
+
}
|
|
94
|
+
FileReader.EMPTY = 0;
|
|
95
|
+
FileReader.LOADING = 1;
|
|
96
|
+
FileReader.DONE = 2;
|
|
97
|
+
FileReader.prototype.addEventListener = function(type, cb) {
|
|
98
|
+
if (!this._listeners[type]) this._listeners[type] = [];
|
|
99
|
+
if (this._listeners[type].indexOf(cb) === -1) this._listeners[type].push(cb);
|
|
100
|
+
};
|
|
101
|
+
FileReader.prototype.removeEventListener = function(type, cb) {
|
|
102
|
+
var list = this._listeners[type];
|
|
103
|
+
if (!list) return;
|
|
104
|
+
var idx = list.indexOf(cb);
|
|
105
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
106
|
+
};
|
|
107
|
+
FileReader.prototype._fire = function(type) {
|
|
108
|
+
var evt = { type: type, target: this };
|
|
109
|
+
var handlerProp = 'on' + type;
|
|
110
|
+
if (typeof this[handlerProp] === 'function') this[handlerProp](evt);
|
|
111
|
+
var list = this._listeners[type];
|
|
112
|
+
if (list) list.slice().forEach(function(cb) { cb(evt); });
|
|
113
|
+
};
|
|
114
|
+
FileReader.prototype._read = function(blob, mode) {
|
|
115
|
+
var self = this;
|
|
116
|
+
self.readyState = 1;
|
|
117
|
+
setTimeout(function() {
|
|
118
|
+
if (!blob) {
|
|
119
|
+
self.error = new Error('FileReader was given no blob to read');
|
|
120
|
+
self.readyState = 2;
|
|
121
|
+
self._fire('error');
|
|
122
|
+
self._fire('loadend');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
var bytes = blob._bytes || [];
|
|
126
|
+
if (mode === 'text') {
|
|
127
|
+
var buf = new ArrayBuffer(bytes.length);
|
|
128
|
+
var view = new Uint8Array(buf);
|
|
129
|
+
for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
|
|
130
|
+
self.result = new TextDecoder().decode(buf);
|
|
131
|
+
} else if (mode === 'dataurl') {
|
|
132
|
+
var binary = '';
|
|
133
|
+
for (var j = 0; j < bytes.length; j++) binary += String.fromCharCode(bytes[j]);
|
|
134
|
+
self.result = 'data:' + (blob.type || '') + ';base64,' + btoa(binary);
|
|
135
|
+
} else if (mode === 'arraybuffer') {
|
|
136
|
+
var arrBuf = new ArrayBuffer(bytes.length);
|
|
137
|
+
var arrView = new Uint8Array(arrBuf);
|
|
138
|
+
for (var k = 0; k < bytes.length; k++) arrView[k] = bytes[k];
|
|
139
|
+
self.result = arrBuf;
|
|
140
|
+
}
|
|
141
|
+
self.readyState = 2;
|
|
142
|
+
self._fire('load');
|
|
143
|
+
self._fire('loadend');
|
|
144
|
+
}, 0);
|
|
145
|
+
};
|
|
146
|
+
FileReader.prototype.readAsText = function(blob) { this._read(blob, 'text'); };
|
|
147
|
+
FileReader.prototype.readAsDataURL = function(blob) { this._read(blob, 'dataurl'); };
|
|
148
|
+
FileReader.prototype.readAsArrayBuffer = function(blob) { this._read(blob, 'arraybuffer'); };
|
|
149
|
+
globalThis.FileReader = FileReader;
|
|
150
|
+
})();
|
|
151
|
+
)JS";
|
|
152
|
+
|
|
153
|
+
} // namespace
|
|
154
|
+
|
|
155
|
+
void BlobBindings::install(JSContext* ctx) {
|
|
156
|
+
JSValue result = JS_Eval(ctx, kBlobBootstrapScript, strlen(kBlobBootstrapScript),
|
|
157
|
+
"<blob-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
158
|
+
if (JS_IsException(result)) {
|
|
159
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
160
|
+
}
|
|
161
|
+
JS_FreeValue(ctx, result);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "quickjs.h"
|
|
4
|
+
|
|
5
|
+
namespace margelo::nitro::nitrojsdom {
|
|
6
|
+
|
|
7
|
+
// Registers globalThis.Blob, globalThis.File, and globalThis.FileReader.
|
|
8
|
+
// Pure JS on top of TextEncoder/TextDecoder and btoa, so must run after
|
|
9
|
+
// TextEncodingBindings and WindowBindings.
|
|
10
|
+
struct BlobBindings {
|
|
11
|
+
static void install(JSContext* ctx);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#include "CSSOMBindings.hpp"
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
namespace margelo::nitro::nitrojsdom {
|
|
5
|
+
|
|
6
|
+
namespace {
|
|
7
|
+
|
|
8
|
+
const char* kCSSOMBootstrapScript = R"JS(
|
|
9
|
+
(function() {
|
|
10
|
+
function stripComments(text) {
|
|
11
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function camelToKebab(name) {
|
|
15
|
+
return name.replace(/[A-Z]/g, function(c) { return '-' + c.toLowerCase(); });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Splits a stylesheet's raw text into top-level rule descriptors, tracking
|
|
19
|
+
// brace depth so a nested block (e.g. the rules inside @media) is captured
|
|
20
|
+
// as one opaque chunk rather than mis-parsed as a sibling rule.
|
|
21
|
+
function splitTopLevelRules(cssText) {
|
|
22
|
+
var text = stripComments(String(cssText === undefined ? '' : cssText));
|
|
23
|
+
var descriptors = [];
|
|
24
|
+
var i = 0;
|
|
25
|
+
var len = text.length;
|
|
26
|
+
while (i < len) {
|
|
27
|
+
while (i < len && /\s/.test(text.charAt(i))) i++;
|
|
28
|
+
if (i >= len) break;
|
|
29
|
+
var start = i;
|
|
30
|
+
var depth = 0;
|
|
31
|
+
var j = i;
|
|
32
|
+
while (j < len) {
|
|
33
|
+
var c = text.charAt(j);
|
|
34
|
+
if (c === '{') {
|
|
35
|
+
depth++;
|
|
36
|
+
} else if (c === '}') {
|
|
37
|
+
depth--;
|
|
38
|
+
if (depth <= 0) { j++; break; }
|
|
39
|
+
} else if (c === ';' && depth === 0) {
|
|
40
|
+
j++;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
j++;
|
|
44
|
+
}
|
|
45
|
+
var raw = text.slice(start, j);
|
|
46
|
+
if (raw.trim()) descriptors.push(parseOneRule(raw));
|
|
47
|
+
i = j;
|
|
48
|
+
}
|
|
49
|
+
return descriptors;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseOneRule(raw) {
|
|
53
|
+
var trimmed = raw.trim();
|
|
54
|
+
var braceIdx = trimmed.indexOf('{');
|
|
55
|
+
if (trimmed.charAt(0) === '@' || braceIdx === -1) {
|
|
56
|
+
return { kind: 'unknown', cssText: trimmed };
|
|
57
|
+
}
|
|
58
|
+
var selectorText = trimmed.slice(0, braceIdx).trim();
|
|
59
|
+
var body = trimmed.slice(braceIdx + 1, trimmed.length - 1);
|
|
60
|
+
return { kind: 'style', selectorText: selectorText, declarationText: body.trim() };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parses "color: red; font-size: 12px" into ordered [name, value] pairs —
|
|
64
|
+
// same informal model StyleBindings.cpp uses for inline element.style.
|
|
65
|
+
function parseDeclarations(text) {
|
|
66
|
+
var pairs = [];
|
|
67
|
+
var parts = String(text === undefined ? '' : text).split(';');
|
|
68
|
+
for (var i = 0; i < parts.length; i++) {
|
|
69
|
+
var part = parts[i].trim();
|
|
70
|
+
if (!part) continue;
|
|
71
|
+
var colonIdx = part.indexOf(':');
|
|
72
|
+
if (colonIdx === -1) continue;
|
|
73
|
+
var prop = part.slice(0, colonIdx).trim().toLowerCase();
|
|
74
|
+
var value = part.slice(colonIdx + 1).trim();
|
|
75
|
+
if (prop) pairs.push([prop, value]);
|
|
76
|
+
}
|
|
77
|
+
return pairs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeRuleStyle(initialText) {
|
|
81
|
+
var pairs = parseDeclarations(initialText);
|
|
82
|
+
|
|
83
|
+
var target = {
|
|
84
|
+
getPropertyValue: function(name) {
|
|
85
|
+
var kebab = camelToKebab(String(name));
|
|
86
|
+
for (var i = 0; i < pairs.length; i++) if (pairs[i][0] === kebab) return pairs[i][1];
|
|
87
|
+
return '';
|
|
88
|
+
},
|
|
89
|
+
setProperty: function(name, value) {
|
|
90
|
+
var kebab = camelToKebab(String(name));
|
|
91
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
92
|
+
if (pairs[i][0] === kebab) { pairs[i][1] = String(value); return; }
|
|
93
|
+
}
|
|
94
|
+
pairs.push([kebab, String(value)]);
|
|
95
|
+
},
|
|
96
|
+
removeProperty: function(name) {
|
|
97
|
+
var kebab = camelToKebab(String(name));
|
|
98
|
+
var old = '';
|
|
99
|
+
var next = [];
|
|
100
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
101
|
+
if (pairs[i][0] === kebab && !old) { old = pairs[i][1]; continue; }
|
|
102
|
+
next.push(pairs[i]);
|
|
103
|
+
}
|
|
104
|
+
pairs = next;
|
|
105
|
+
return old;
|
|
106
|
+
},
|
|
107
|
+
get cssText() {
|
|
108
|
+
var out = '';
|
|
109
|
+
for (var i = 0; i < pairs.length; i++) out += pairs[i][0] + ': ' + pairs[i][1] + '; ';
|
|
110
|
+
return out.trim();
|
|
111
|
+
},
|
|
112
|
+
set cssText(text) {
|
|
113
|
+
pairs = parseDeclarations(text);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return new Proxy(target, {
|
|
118
|
+
get: function(t, prop, receiver) {
|
|
119
|
+
if (typeof prop === 'string' && !(prop in t)) {
|
|
120
|
+
var kebab = camelToKebab(prop);
|
|
121
|
+
for (var i = 0; i < pairs.length; i++) if (pairs[i][0] === kebab) return pairs[i][1];
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return Reflect.get(t, prop, receiver);
|
|
125
|
+
},
|
|
126
|
+
set: function(t, prop, value, receiver) {
|
|
127
|
+
if (typeof prop === 'string' && !(prop in t)) {
|
|
128
|
+
t.setProperty(prop, value);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return Reflect.set(t, prop, value, receiver);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function CSSRule() {}
|
|
137
|
+
CSSRule.STYLE_RULE = 1;
|
|
138
|
+
CSSRule.MEDIA_RULE = 4;
|
|
139
|
+
CSSRule.prototype.type = 0;
|
|
140
|
+
|
|
141
|
+
function CSSStyleRule(selectorText, declarationText, parentStyleSheet) {
|
|
142
|
+
this.selectorText = selectorText;
|
|
143
|
+
this.style = makeRuleStyle(declarationText);
|
|
144
|
+
this.parentStyleSheet = parentStyleSheet || null;
|
|
145
|
+
this.type = CSSRule.STYLE_RULE;
|
|
146
|
+
}
|
|
147
|
+
CSSStyleRule.prototype = Object.create(CSSRule.prototype);
|
|
148
|
+
CSSStyleRule.prototype.constructor = CSSStyleRule;
|
|
149
|
+
Object.defineProperty(CSSStyleRule.prototype, 'cssText', {
|
|
150
|
+
get: function() { return this.selectorText + ' { ' + this.style.cssText + ' }'; },
|
|
151
|
+
configurable: true,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
function makeUnknownRule(cssText, parentStyleSheet) {
|
|
155
|
+
return { type: 0, cssText: cssText, parentStyleSheet: parentStyleSheet || null };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ruleFromDescriptor(d, sheet) {
|
|
159
|
+
return d.kind === 'style'
|
|
160
|
+
? new CSSStyleRule(d.selectorText, d.declarationText, sheet)
|
|
161
|
+
: makeUnknownRule(d.cssText, sheet);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function makeRuleList(rules) {
|
|
165
|
+
var arr = rules.slice();
|
|
166
|
+
arr.item = function(i) { return this[i] !== undefined ? this[i] : null; };
|
|
167
|
+
return arr;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function CSSStyleSheet(ownerNode) {
|
|
171
|
+
this.ownerNode = ownerNode || null;
|
|
172
|
+
this.cssRules = makeRuleList([]);
|
|
173
|
+
this.rules = this.cssRules;
|
|
174
|
+
}
|
|
175
|
+
CSSStyleSheet.prototype.insertRule = function(ruleText, index) {
|
|
176
|
+
var idx = (index === undefined) ? this.cssRules.length : index;
|
|
177
|
+
if (idx < 0 || idx > this.cssRules.length) {
|
|
178
|
+
throw new DOMException(
|
|
179
|
+
'The index provided (' + idx + ') is not in the allowed range.', 'IndexSizeError');
|
|
180
|
+
}
|
|
181
|
+
var rule = ruleFromDescriptor(parseOneRule(String(ruleText)), this);
|
|
182
|
+
Array.prototype.splice.call(this.cssRules, idx, 0, rule);
|
|
183
|
+
return idx;
|
|
184
|
+
};
|
|
185
|
+
CSSStyleSheet.prototype.deleteRule = function(index) {
|
|
186
|
+
if (index < 0 || index >= this.cssRules.length) {
|
|
187
|
+
throw new DOMException(
|
|
188
|
+
'The index provided (' + index + ') is not in the allowed range.', 'IndexSizeError');
|
|
189
|
+
}
|
|
190
|
+
Array.prototype.splice.call(this.cssRules, index, 1);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function parseStylesheetInto(sheet, cssText) {
|
|
194
|
+
var descriptors = splitTopLevelRules(cssText);
|
|
195
|
+
for (var i = 0; i < descriptors.length; i++) {
|
|
196
|
+
sheet.cssRules.push(ruleFromDescriptor(descriptors[i], sheet));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
globalThis.CSSRule = CSSRule;
|
|
201
|
+
globalThis.CSSStyleRule = CSSStyleRule;
|
|
202
|
+
globalThis.CSSStyleSheet = CSSStyleSheet;
|
|
203
|
+
|
|
204
|
+
Object.defineProperty(Element.prototype, 'sheet', {
|
|
205
|
+
get: function() {
|
|
206
|
+
if (this.tagName !== 'STYLE') return null;
|
|
207
|
+
if (!this._sheet) {
|
|
208
|
+
var sheet = new CSSStyleSheet(this);
|
|
209
|
+
parseStylesheetInto(sheet, this.textContent);
|
|
210
|
+
this._sheet = sheet;
|
|
211
|
+
}
|
|
212
|
+
return this._sheet;
|
|
213
|
+
},
|
|
214
|
+
configurable: true,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
Object.defineProperty(document, 'styleSheets', {
|
|
218
|
+
get: function() {
|
|
219
|
+
var styleEls = document.querySelectorAll('style');
|
|
220
|
+
var sheets = [];
|
|
221
|
+
for (var i = 0; i < styleEls.length; i++) {
|
|
222
|
+
var sheet = styleEls[i].sheet;
|
|
223
|
+
if (sheet) sheets.push(sheet);
|
|
224
|
+
}
|
|
225
|
+
sheets.item = function(i) { return this[i] !== undefined ? this[i] : null; };
|
|
226
|
+
return sheets;
|
|
227
|
+
},
|
|
228
|
+
configurable: true,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── getComputedStyle ──────────────────────────────────────────────────
|
|
232
|
+
// No layout/cascade engine backs this sandbox — there is no stylesheet
|
|
233
|
+
// specificity resolution or inheritance, only the element's own inline
|
|
234
|
+
// `style` attribute. `display` falls back to a small user-agent-stylesheet
|
|
235
|
+
// approximation (block/inline/inline-block/none by tag name, or 'none' for
|
|
236
|
+
// a `hidden` attribute) since "is this element visible" is the one
|
|
237
|
+
// getComputedStyle check real-world embedded scripts actually make;
|
|
238
|
+
// `visibility`/`opacity` fall back to their initial values. Every other
|
|
239
|
+
// unset property returns '', not a computed initial value.
|
|
240
|
+
var kBlockTags = { ADDRESS: 1, ARTICLE: 1, ASIDE: 1, BLOCKQUOTE: 1, DETAILS: 1, DIALOG: 1, DD: 1, DIV: 1,
|
|
241
|
+
DL: 1, DT: 1, FIELDSET: 1, FIGCAPTION: 1, FIGURE: 1, FOOTER: 1, FORM: 1, H1: 1, H2: 1, H3: 1, H4: 1,
|
|
242
|
+
H5: 1, H6: 1, HEADER: 1, HGROUP: 1, HR: 1, LI: 1, MAIN: 1, NAV: 1, OL: 1, P: 1, PRE: 1, SECTION: 1,
|
|
243
|
+
TABLE: 1, UL: 1, HTML: 1, BODY: 1 };
|
|
244
|
+
var kNoneTags = { SCRIPT: 1, STYLE: 1, HEAD: 1, TITLE: 1, META: 1, LINK: 1, TEMPLATE: 1 };
|
|
245
|
+
var kInlineBlockTags = { IMG: 1, BUTTON: 1, INPUT: 1, SELECT: 1, TEXTAREA: 1 };
|
|
246
|
+
|
|
247
|
+
function defaultDisplay(tagName) {
|
|
248
|
+
if (kNoneTags[tagName]) return 'none';
|
|
249
|
+
if (kInlineBlockTags[tagName]) return 'inline-block';
|
|
250
|
+
if (kBlockTags[tagName]) return 'block';
|
|
251
|
+
return 'inline';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveProperty(el, kebabName) {
|
|
255
|
+
var v = el.style.getPropertyValue(kebabName);
|
|
256
|
+
if (v) return v;
|
|
257
|
+
if (kebabName === 'display') return el.hasAttribute('hidden') ? 'none' : defaultDisplay(el.tagName);
|
|
258
|
+
if (kebabName === 'visibility') return 'visible';
|
|
259
|
+
if (kebabName === 'opacity') return '1';
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
globalThis.getComputedStyle = function(el) {
|
|
264
|
+
if (!el || typeof el.tagName !== 'string') {
|
|
265
|
+
throw new TypeError("Failed to execute 'getComputedStyle': parameter 1 is not of type 'Element'.");
|
|
266
|
+
}
|
|
267
|
+
return new Proxy({}, {
|
|
268
|
+
get: function(_target, prop) {
|
|
269
|
+
if (prop === 'getPropertyValue') return function(name) { return resolveProperty(el, String(name)); };
|
|
270
|
+
if (prop === 'getPropertyPriority') return function() { return ''; };
|
|
271
|
+
if (typeof prop !== 'string') return undefined;
|
|
272
|
+
return resolveProperty(el, camelToKebab(prop));
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
})();
|
|
277
|
+
)JS";
|
|
278
|
+
|
|
279
|
+
} // namespace
|
|
280
|
+
|
|
281
|
+
void CSSOMBindings::install(JSContext* ctx) {
|
|
282
|
+
JSValue result = JS_Eval(ctx, kCSSOMBootstrapScript, strlen(kCSSOMBootstrapScript),
|
|
283
|
+
"<cssom-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
284
|
+
if (JS_IsException(result)) {
|
|
285
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
286
|
+
}
|
|
287
|
+
JS_FreeValue(ctx, result);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "quickjs.h"
|
|
4
|
+
|
|
5
|
+
namespace margelo::nitro::nitrojsdom {
|
|
6
|
+
|
|
7
|
+
// Registers a minimal CSSOM: globalThis.CSSRule/CSSStyleRule/CSSStyleSheet,
|
|
8
|
+
// Element.prototype.sheet (tag-checked for "style"), document.styleSheets,
|
|
9
|
+
// and globalThis.getComputedStyle (inline-style + tag-default-display only,
|
|
10
|
+
// no real cascade — see the bootstrap script for the exact fallback rules).
|
|
11
|
+
//
|
|
12
|
+
// Rules are split by a hand-rolled top-level brace scanner rather than
|
|
13
|
+
// Lexbor's CSS rule/declaration AST (lxb_css_rule_t) — mirrors how
|
|
14
|
+
// StyleBindings.cpp already treats inline "style" text as informally-parsed
|
|
15
|
+
// property:value pairs rather than integrating Lexbor's typed CSS value
|
|
16
|
+
// engine. At-rules (@media, @keyframes, ...) are captured as opaque stub
|
|
17
|
+
// rules (cssText only, no nested-rule access) rather than fully parsed.
|
|
18
|
+
//
|
|
19
|
+
// Must run after ElementBindings (globalThis.Element) and DocumentBindings
|
|
20
|
+
// (globalThis.document).
|
|
21
|
+
struct CSSOMBindings {
|
|
22
|
+
static void install(JSContext* ctx);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "ClassListBindings.hpp"
|
|
2
|
+
#include "DOMExceptionBindings.hpp"
|
|
2
3
|
#include "../DOMBindingsInternal.hpp"
|
|
3
4
|
#include "../QuickJSRuntime.hpp"
|
|
4
5
|
#include "../MutationObservers.hpp"
|
|
@@ -36,13 +37,13 @@ std::string join_classes(const std::vector<std::string>& v) {
|
|
|
36
37
|
// Validates a DOMTokenList token: throws on empty or whitespace-containing token.
|
|
37
38
|
bool validate_token(JSContext* ctx, const char* token) {
|
|
38
39
|
if (!token || token[0] == '\0') {
|
|
39
|
-
|
|
40
|
+
throw_dom_exception(ctx, "SyntaxError", "The token provided must not be empty.");
|
|
40
41
|
return false;
|
|
41
42
|
}
|
|
42
43
|
for (const char* p = token; *p; p++) {
|
|
43
44
|
unsigned char c = (unsigned char)*p;
|
|
44
45
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f') {
|
|
45
|
-
|
|
46
|
+
throw_dom_exception(ctx, "InvalidCharacterError", "The token provided contains HTML space characters, which are not valid in tokens.");
|
|
46
47
|
return false;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#include "CookieBindings.hpp"
|
|
2
|
+
#include "../DOMBindingsInternal.hpp"
|
|
3
|
+
#include "../QuickJSRuntime.hpp"
|
|
4
|
+
#include "../Storage.hpp"
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <cctype>
|
|
7
|
+
#include <cstdlib>
|
|
8
|
+
#include <ctime>
|
|
9
|
+
|
|
10
|
+
namespace margelo::nitro::nitrojsdom {
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
std::string trim(const std::string& s) {
|
|
15
|
+
size_t start = s.find_first_not_of(" \t\n\r");
|
|
16
|
+
if (start == std::string::npos) return "";
|
|
17
|
+
size_t end = s.find_last_not_of(" \t\n\r");
|
|
18
|
+
return s.substr(start, end - start + 1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
std::string to_lower(std::string s) {
|
|
22
|
+
for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Best-effort: accepts the date formats real cookie strings actually use
|
|
27
|
+
// (RFC 1123, RFC 850, asctime) — not a full HTTP-date parser.
|
|
28
|
+
bool is_past_date(const std::string& value) {
|
|
29
|
+
static const char* kFormats[] = {
|
|
30
|
+
"%a, %d %b %Y %H:%M:%S", // RFC 1123: "Thu, 01 Jan 1970 00:00:00 GMT"
|
|
31
|
+
"%A, %d-%b-%y %H:%M:%S", // RFC 850
|
|
32
|
+
"%a %b %d %H:%M:%S %Y", // asctime
|
|
33
|
+
};
|
|
34
|
+
for (const char* fmt : kFormats) {
|
|
35
|
+
struct tm parsed{};
|
|
36
|
+
if (strptime(value.c_str(), fmt, &parsed)) {
|
|
37
|
+
return timegm(&parsed) <= time(nullptr);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A cookie write is a deletion when `max-age` is <= 0 or `expires` names a
|
|
44
|
+
// date in the past — the two idioms real-world scripts use to clear a
|
|
45
|
+
// cookie (most commonly `expires=Thu, 01 Jan 1970 00:00:00 GMT`).
|
|
46
|
+
bool is_deletion(const std::string& attrs) {
|
|
47
|
+
size_t pos = 0;
|
|
48
|
+
while (pos < attrs.size()) {
|
|
49
|
+
size_t semi = attrs.find(';', pos);
|
|
50
|
+
std::string attr = attrs.substr(pos, semi == std::string::npos ? std::string::npos : semi - pos);
|
|
51
|
+
pos = semi == std::string::npos ? attrs.size() : semi + 1;
|
|
52
|
+
|
|
53
|
+
size_t eq = attr.find('=');
|
|
54
|
+
if (eq == std::string::npos) continue;
|
|
55
|
+
std::string key = to_lower(trim(attr.substr(0, eq)));
|
|
56
|
+
std::string value = trim(attr.substr(eq + 1));
|
|
57
|
+
|
|
58
|
+
if (key == "max-age") {
|
|
59
|
+
if (std::atol(value.c_str()) <= 0) return true;
|
|
60
|
+
} else if (key == "expires") {
|
|
61
|
+
if (is_past_date(value)) return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
JSValue js_doc_get_cookie(JSContext* ctx, JSValue) {
|
|
68
|
+
auto* rctx = get_ctx(ctx);
|
|
69
|
+
if (!rctx) return JS_NewString(ctx, "");
|
|
70
|
+
|
|
71
|
+
std::string result;
|
|
72
|
+
for (size_t i = 0; i < rctx->cookie_jar.length(); i++) {
|
|
73
|
+
auto key = rctx->cookie_jar.key(i);
|
|
74
|
+
if (!key) continue;
|
|
75
|
+
auto value = rctx->cookie_jar.getItem(*key);
|
|
76
|
+
if (!value) continue;
|
|
77
|
+
if (!result.empty()) result += "; ";
|
|
78
|
+
result += *key + "=" + *value;
|
|
79
|
+
}
|
|
80
|
+
return JS_NewStringLen(ctx, result.data(), result.size());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
JSValue js_doc_set_cookie(JSContext* ctx, JSValue, JSValue val) {
|
|
84
|
+
auto* rctx = get_ctx(ctx);
|
|
85
|
+
const char* str = JS_ToCString(ctx, val);
|
|
86
|
+
if (str && rctx) {
|
|
87
|
+
std::string input(str);
|
|
88
|
+
size_t semi = input.find(';');
|
|
89
|
+
std::string pair = semi == std::string::npos ? input : input.substr(0, semi);
|
|
90
|
+
std::string attrs = semi == std::string::npos ? "" : input.substr(semi + 1);
|
|
91
|
+
size_t eq = pair.find('=');
|
|
92
|
+
if (eq != std::string::npos) {
|
|
93
|
+
std::string name = trim(pair.substr(0, eq));
|
|
94
|
+
std::string value = trim(pair.substr(eq + 1));
|
|
95
|
+
if (!name.empty()) {
|
|
96
|
+
if (is_deletion(attrs)) {
|
|
97
|
+
rctx->cookie_jar.removeItem(name);
|
|
98
|
+
} else {
|
|
99
|
+
rctx->cookie_jar.setItem(name, value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (str) JS_FreeCString(ctx, str);
|
|
105
|
+
return JS_UNDEFINED;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} // namespace
|
|
109
|
+
|
|
110
|
+
void CookieBindings::install(JSContext* ctx) {
|
|
111
|
+
JSValue global = JS_GetGlobalObject(ctx);
|
|
112
|
+
JSValue doc = JS_GetPropertyStr(ctx, global, "document");
|
|
113
|
+
define_prop(ctx, doc, "cookie", js_doc_get_cookie, js_doc_set_cookie);
|
|
114
|
+
JS_FreeValue(ctx, doc);
|
|
115
|
+
JS_FreeValue(ctx, global);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "quickjs.h"
|
|
4
|
+
|
|
5
|
+
namespace margelo::nitro::nitrojsdom {
|
|
6
|
+
|
|
7
|
+
// Registers document.cookie (get/set), backed by an in-memory per-runtime
|
|
8
|
+
// cookie jar (RuntimeContext::cookie_jar — a name -> value map preserving
|
|
9
|
+
// insertion order). jsdom backs this with a real tough-cookie CookieJar
|
|
10
|
+
// scoped by domain/path/expiry; this sandbox has no navigation or multiple
|
|
11
|
+
// origins, so scoping attributes (path/domain/secure/samesite) are parsed
|
|
12
|
+
// off and discarded rather than enforced. `expires`/`max-age` are the
|
|
13
|
+
// exception: a value in the past (or max-age <= 0) is treated as the
|
|
14
|
+
// deletion idiom real-world scripts use it for, and removes the cookie
|
|
15
|
+
// instead of leaving a stray "name=" entry in the jar. `expires` parsing is
|
|
16
|
+
// best-effort (RFC 1123/850/asctime formats only, not a full HTTP-date parser).
|
|
17
|
+
//
|
|
18
|
+
// Must run after DocumentBindings (needs globalThis.document to exist).
|
|
19
|
+
struct CookieBindings {
|
|
20
|
+
static void install(JSContext* ctx);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
} // namespace margelo::nitro::nitrojsdom
|