@aquiles-ai/renderize 1.7.0 → 1.85.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/dist/index.cjs +103 -3
- package/dist/index.js +104 -4
- package/package.json +1 -1
- package/src/Renderize.tsx +50 -12
- package/src/template.ts +64 -1
package/dist/index.cjs
CHANGED
|
@@ -88,8 +88,71 @@ function buildTemplate(code) {
|
|
|
88
88
|
<body>
|
|
89
89
|
<div id="root"></div>
|
|
90
90
|
|
|
91
|
+
<script>
|
|
92
|
+
// \u2500\u2500 FETCH PROXY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
93
|
+
// Override window.fetch to proxy requests through the parent window.
|
|
94
|
+
// This solves the CORS/null-origin issue with srcdoc iframes:
|
|
95
|
+
// the parent has a real origin and can make fetch calls freely.
|
|
96
|
+
window.fetch = function(url, options = {}) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
99
|
+
|
|
100
|
+
// Serialize body \u2014 postMessage can't transfer Request objects
|
|
101
|
+
const serializedOptions = {
|
|
102
|
+
method: options.method || "GET",
|
|
103
|
+
headers: options.headers || {},
|
|
104
|
+
body: options.body || null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Listen for the response from the parent
|
|
108
|
+
function handleMessage(event) {
|
|
109
|
+
if (
|
|
110
|
+
event.data?.source !== "renderize" ||
|
|
111
|
+
event.data?.type !== "fetch-response" ||
|
|
112
|
+
event.data?.id !== id
|
|
113
|
+
) return;
|
|
114
|
+
|
|
115
|
+
window.removeEventListener("message", handleMessage);
|
|
116
|
+
|
|
117
|
+
if (event.data.error) {
|
|
118
|
+
reject(new Error(event.data.error));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Reconstruct a real Response object from the serialized data
|
|
123
|
+
const { status, statusText, headers, body } = event.data;
|
|
124
|
+
const responseBody = typeof body === "string" ? body : JSON.stringify(body);
|
|
125
|
+
|
|
126
|
+
const response = new Response(responseBody, {
|
|
127
|
+
status,
|
|
128
|
+
statusText,
|
|
129
|
+
headers: new Headers(headers),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
resolve(response);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
window.addEventListener("message", handleMessage);
|
|
136
|
+
|
|
137
|
+
// Ask the parent to perform the fetch on our behalf
|
|
138
|
+
window.parent.postMessage({
|
|
139
|
+
source: "renderize",
|
|
140
|
+
type: "fetch-request",
|
|
141
|
+
id,
|
|
142
|
+
url,
|
|
143
|
+
options: serializedOptions,
|
|
144
|
+
}, "*");
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
// \u2500\u2500 END FETCH PROXY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
148
|
+
</script>
|
|
149
|
+
|
|
91
150
|
<script type="text/babel" data-type="module">
|
|
92
|
-
import React, {
|
|
151
|
+
import React, {
|
|
152
|
+
useState, useEffect, useRef, useCallback,
|
|
153
|
+
useMemo, useReducer, useContext, createContext,
|
|
154
|
+
forwardRef, Fragment
|
|
155
|
+
} from "react";
|
|
93
156
|
import { createRoot } from "react-dom/client";
|
|
94
157
|
|
|
95
158
|
// \u2500\u2500 USER CODE START \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -113,14 +176,50 @@ function Renderize({
|
|
|
113
176
|
style,
|
|
114
177
|
onError
|
|
115
178
|
}) {
|
|
179
|
+
const iframeRef = (0, import_react.useRef)(null);
|
|
116
180
|
const [srcDoc, setSrcDoc] = (0, import_react.useState)(null);
|
|
117
181
|
(0, import_react.useEffect)(() => {
|
|
118
182
|
if (!code?.trim()) return;
|
|
119
183
|
setSrcDoc(buildTemplate(code));
|
|
120
184
|
}, [code]);
|
|
121
185
|
(0, import_react.useEffect)(() => {
|
|
122
|
-
const handler = (event) => {
|
|
123
|
-
if (event.data?.source
|
|
186
|
+
const handler = async (event) => {
|
|
187
|
+
if (event.data?.source !== "renderize") return;
|
|
188
|
+
if (event.data.type === "fetch-request") {
|
|
189
|
+
const { id, url, options } = event.data;
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch(url, {
|
|
192
|
+
method: options.method,
|
|
193
|
+
headers: options.headers,
|
|
194
|
+
body: options.body ?? void 0
|
|
195
|
+
});
|
|
196
|
+
const body = await res.text();
|
|
197
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
198
|
+
{
|
|
199
|
+
source: "renderize",
|
|
200
|
+
type: "fetch-response",
|
|
201
|
+
id,
|
|
202
|
+
status: res.status,
|
|
203
|
+
statusText: res.statusText,
|
|
204
|
+
// Convert Headers to a plain object for structured clone
|
|
205
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
206
|
+
body
|
|
207
|
+
},
|
|
208
|
+
"*"
|
|
209
|
+
);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
212
|
+
{
|
|
213
|
+
source: "renderize",
|
|
214
|
+
type: "fetch-response",
|
|
215
|
+
id,
|
|
216
|
+
error: err instanceof Error ? err.message : String(err)
|
|
217
|
+
},
|
|
218
|
+
"*"
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (event.data.type === "error" && onError) {
|
|
124
223
|
onError(event.data.message);
|
|
125
224
|
}
|
|
126
225
|
};
|
|
@@ -135,6 +234,7 @@ function Renderize({
|
|
|
135
234
|
children: srcDoc && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
136
235
|
"iframe",
|
|
137
236
|
{
|
|
237
|
+
ref: iframeRef,
|
|
138
238
|
srcDoc,
|
|
139
239
|
title: "Renderize Sandbox",
|
|
140
240
|
sandbox: "allow-scripts allow-forms allow-modals allow-popups allow-downloads",
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/Renderize.tsx
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
|
|
4
4
|
// src/template.ts
|
|
5
5
|
function buildTemplate(code) {
|
|
@@ -62,8 +62,71 @@ function buildTemplate(code) {
|
|
|
62
62
|
<body>
|
|
63
63
|
<div id="root"></div>
|
|
64
64
|
|
|
65
|
+
<script>
|
|
66
|
+
// \u2500\u2500 FETCH PROXY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
67
|
+
// Override window.fetch to proxy requests through the parent window.
|
|
68
|
+
// This solves the CORS/null-origin issue with srcdoc iframes:
|
|
69
|
+
// the parent has a real origin and can make fetch calls freely.
|
|
70
|
+
window.fetch = function(url, options = {}) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
73
|
+
|
|
74
|
+
// Serialize body \u2014 postMessage can't transfer Request objects
|
|
75
|
+
const serializedOptions = {
|
|
76
|
+
method: options.method || "GET",
|
|
77
|
+
headers: options.headers || {},
|
|
78
|
+
body: options.body || null,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Listen for the response from the parent
|
|
82
|
+
function handleMessage(event) {
|
|
83
|
+
if (
|
|
84
|
+
event.data?.source !== "renderize" ||
|
|
85
|
+
event.data?.type !== "fetch-response" ||
|
|
86
|
+
event.data?.id !== id
|
|
87
|
+
) return;
|
|
88
|
+
|
|
89
|
+
window.removeEventListener("message", handleMessage);
|
|
90
|
+
|
|
91
|
+
if (event.data.error) {
|
|
92
|
+
reject(new Error(event.data.error));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Reconstruct a real Response object from the serialized data
|
|
97
|
+
const { status, statusText, headers, body } = event.data;
|
|
98
|
+
const responseBody = typeof body === "string" ? body : JSON.stringify(body);
|
|
99
|
+
|
|
100
|
+
const response = new Response(responseBody, {
|
|
101
|
+
status,
|
|
102
|
+
statusText,
|
|
103
|
+
headers: new Headers(headers),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
resolve(response);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
window.addEventListener("message", handleMessage);
|
|
110
|
+
|
|
111
|
+
// Ask the parent to perform the fetch on our behalf
|
|
112
|
+
window.parent.postMessage({
|
|
113
|
+
source: "renderize",
|
|
114
|
+
type: "fetch-request",
|
|
115
|
+
id,
|
|
116
|
+
url,
|
|
117
|
+
options: serializedOptions,
|
|
118
|
+
}, "*");
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
// \u2500\u2500 END FETCH PROXY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
122
|
+
</script>
|
|
123
|
+
|
|
65
124
|
<script type="text/babel" data-type="module">
|
|
66
|
-
import React, {
|
|
125
|
+
import React, {
|
|
126
|
+
useState, useEffect, useRef, useCallback,
|
|
127
|
+
useMemo, useReducer, useContext, createContext,
|
|
128
|
+
forwardRef, Fragment
|
|
129
|
+
} from "react";
|
|
67
130
|
import { createRoot } from "react-dom/client";
|
|
68
131
|
|
|
69
132
|
// \u2500\u2500 USER CODE START \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -87,14 +150,50 @@ function Renderize({
|
|
|
87
150
|
style,
|
|
88
151
|
onError
|
|
89
152
|
}) {
|
|
153
|
+
const iframeRef = useRef(null);
|
|
90
154
|
const [srcDoc, setSrcDoc] = useState(null);
|
|
91
155
|
useEffect(() => {
|
|
92
156
|
if (!code?.trim()) return;
|
|
93
157
|
setSrcDoc(buildTemplate(code));
|
|
94
158
|
}, [code]);
|
|
95
159
|
useEffect(() => {
|
|
96
|
-
const handler = (event) => {
|
|
97
|
-
if (event.data?.source
|
|
160
|
+
const handler = async (event) => {
|
|
161
|
+
if (event.data?.source !== "renderize") return;
|
|
162
|
+
if (event.data.type === "fetch-request") {
|
|
163
|
+
const { id, url, options } = event.data;
|
|
164
|
+
try {
|
|
165
|
+
const res = await fetch(url, {
|
|
166
|
+
method: options.method,
|
|
167
|
+
headers: options.headers,
|
|
168
|
+
body: options.body ?? void 0
|
|
169
|
+
});
|
|
170
|
+
const body = await res.text();
|
|
171
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
172
|
+
{
|
|
173
|
+
source: "renderize",
|
|
174
|
+
type: "fetch-response",
|
|
175
|
+
id,
|
|
176
|
+
status: res.status,
|
|
177
|
+
statusText: res.statusText,
|
|
178
|
+
// Convert Headers to a plain object for structured clone
|
|
179
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
180
|
+
body
|
|
181
|
+
},
|
|
182
|
+
"*"
|
|
183
|
+
);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
186
|
+
{
|
|
187
|
+
source: "renderize",
|
|
188
|
+
type: "fetch-response",
|
|
189
|
+
id,
|
|
190
|
+
error: err instanceof Error ? err.message : String(err)
|
|
191
|
+
},
|
|
192
|
+
"*"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (event.data.type === "error" && onError) {
|
|
98
197
|
onError(event.data.message);
|
|
99
198
|
}
|
|
100
199
|
};
|
|
@@ -109,6 +208,7 @@ function Renderize({
|
|
|
109
208
|
children: srcDoc && /* @__PURE__ */ jsx(
|
|
110
209
|
"iframe",
|
|
111
210
|
{
|
|
211
|
+
ref: iframeRef,
|
|
112
212
|
srcDoc,
|
|
113
213
|
title: "Renderize Sandbox",
|
|
114
214
|
sandbox: "allow-scripts allow-forms allow-modals allow-popups allow-downloads",
|
package/package.json
CHANGED
package/src/Renderize.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { buildTemplate } from "./template.js";
|
|
4
4
|
|
|
5
5
|
export interface RenderizeProps {
|
|
@@ -25,26 +25,65 @@ export function Renderize({
|
|
|
25
25
|
style,
|
|
26
26
|
onError,
|
|
27
27
|
}: RenderizeProps) {
|
|
28
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
28
29
|
const [srcDoc, setSrcDoc] = useState<string | null>(null);
|
|
29
30
|
|
|
30
31
|
useEffect(() => {
|
|
31
32
|
if (!code?.trim()) return;
|
|
32
|
-
// srcdoc works with sandbox="allow-scripts" — no blob URL needed,
|
|
33
|
-
// no same-origin restriction, content is inlined directly
|
|
34
33
|
setSrcDoc(buildTemplate(code));
|
|
35
34
|
}, [code]);
|
|
36
35
|
|
|
37
|
-
//
|
|
36
|
+
// Central message handler — handles both fetch proxying and error forwarding
|
|
38
37
|
useEffect(() => {
|
|
39
|
-
const handler = (event: MessageEvent) => {
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
const handler = async (event: MessageEvent) => {
|
|
39
|
+
if (event.data?.source !== "renderize") return;
|
|
40
|
+
|
|
41
|
+
// ── Fetch proxy ────────────────────────────────────────────────
|
|
42
|
+
if (event.data.type === "fetch-request") {
|
|
43
|
+
const { id, url, options } = event.data;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: options.method,
|
|
48
|
+
headers: options.headers,
|
|
49
|
+
body: options.body ?? undefined,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Serialize the response body as text — the iframe will parse it
|
|
53
|
+
const body = await res.text();
|
|
54
|
+
|
|
55
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
56
|
+
{
|
|
57
|
+
source: "renderize",
|
|
58
|
+
type: "fetch-response",
|
|
59
|
+
id,
|
|
60
|
+
status: res.status,
|
|
61
|
+
statusText: res.statusText,
|
|
62
|
+
// Convert Headers to a plain object for structured clone
|
|
63
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
64
|
+
body,
|
|
65
|
+
},
|
|
66
|
+
"*"
|
|
67
|
+
);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
70
|
+
{
|
|
71
|
+
source: "renderize",
|
|
72
|
+
type: "fetch-response",
|
|
73
|
+
id,
|
|
74
|
+
error: err instanceof Error ? err.message : String(err),
|
|
75
|
+
},
|
|
76
|
+
"*"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Error forwarding ───────────────────────────────────────────
|
|
82
|
+
if (event.data.type === "error" && onError) {
|
|
45
83
|
onError(event.data.message);
|
|
46
84
|
}
|
|
47
85
|
};
|
|
86
|
+
|
|
48
87
|
window.addEventListener("message", handler);
|
|
49
88
|
return () => window.removeEventListener("message", handler);
|
|
50
89
|
}, [onError]);
|
|
@@ -56,9 +95,8 @@ export function Renderize({
|
|
|
56
95
|
>
|
|
57
96
|
{srcDoc && (
|
|
58
97
|
<iframe
|
|
59
|
-
// key forces a full remount on every new render,
|
|
60
|
-
// guaranteeing a clean JS context each time
|
|
61
98
|
key={srcDoc}
|
|
99
|
+
ref={iframeRef}
|
|
62
100
|
srcDoc={srcDoc}
|
|
63
101
|
title="Renderize Sandbox"
|
|
64
102
|
sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads"
|
package/src/template.ts
CHANGED
|
@@ -58,8 +58,71 @@ export function buildTemplate(code: string): string {
|
|
|
58
58
|
<body>
|
|
59
59
|
<div id="root"><\/div>
|
|
60
60
|
|
|
61
|
+
<script>
|
|
62
|
+
// ── FETCH PROXY ──────────────────────────────────────────────────
|
|
63
|
+
// Override window.fetch to proxy requests through the parent window.
|
|
64
|
+
// This solves the CORS/null-origin issue with srcdoc iframes:
|
|
65
|
+
// the parent has a real origin and can make fetch calls freely.
|
|
66
|
+
window.fetch = function(url, options = {}) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
69
|
+
|
|
70
|
+
// Serialize body — postMessage can't transfer Request objects
|
|
71
|
+
const serializedOptions = {
|
|
72
|
+
method: options.method || "GET",
|
|
73
|
+
headers: options.headers || {},
|
|
74
|
+
body: options.body || null,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Listen for the response from the parent
|
|
78
|
+
function handleMessage(event) {
|
|
79
|
+
if (
|
|
80
|
+
event.data?.source !== "renderize" ||
|
|
81
|
+
event.data?.type !== "fetch-response" ||
|
|
82
|
+
event.data?.id !== id
|
|
83
|
+
) return;
|
|
84
|
+
|
|
85
|
+
window.removeEventListener("message", handleMessage);
|
|
86
|
+
|
|
87
|
+
if (event.data.error) {
|
|
88
|
+
reject(new Error(event.data.error));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Reconstruct a real Response object from the serialized data
|
|
93
|
+
const { status, statusText, headers, body } = event.data;
|
|
94
|
+
const responseBody = typeof body === "string" ? body : JSON.stringify(body);
|
|
95
|
+
|
|
96
|
+
const response = new Response(responseBody, {
|
|
97
|
+
status,
|
|
98
|
+
statusText,
|
|
99
|
+
headers: new Headers(headers),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
resolve(response);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
window.addEventListener("message", handleMessage);
|
|
106
|
+
|
|
107
|
+
// Ask the parent to perform the fetch on our behalf
|
|
108
|
+
window.parent.postMessage({
|
|
109
|
+
source: "renderize",
|
|
110
|
+
type: "fetch-request",
|
|
111
|
+
id,
|
|
112
|
+
url,
|
|
113
|
+
options: serializedOptions,
|
|
114
|
+
}, "*");
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
// ── END FETCH PROXY ──────────────────────────────────────────────
|
|
118
|
+
<\/script>
|
|
119
|
+
|
|
61
120
|
<script type="text/babel" data-type="module">
|
|
62
|
-
import React, {
|
|
121
|
+
import React, {
|
|
122
|
+
useState, useEffect, useRef, useCallback,
|
|
123
|
+
useMemo, useReducer, useContext, createContext,
|
|
124
|
+
forwardRef, Fragment
|
|
125
|
+
} from "react";
|
|
63
126
|
import { createRoot } from "react-dom/client";
|
|
64
127
|
|
|
65
128
|
// ── USER CODE START ──────────────────────────────────────────────
|