@ddwang/magnitude-core 0.3.1-ddwang.3 → 0.3.1-ddwang.4
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/agent/browserAgent.js +2 -43
- package/dist/index.cjs +67 -29
- package/dist/index.mjs +67 -29
- package/dist/web/pageContent.d.ts +2 -0
- package/dist/web/pageContent.js +78 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { partitionHtml, serializeToMarkdown } from 'magnitude-extract';
|
|
|
6
6
|
import EventEmitter from "eventemitter3";
|
|
7
7
|
import { retry } from "@/common/retry";
|
|
8
8
|
import { checkOperation } from '@/common/operation';
|
|
9
|
+
import { getVisiblePageContent } from '@/web/pageContent';
|
|
9
10
|
// export interface StartAgentWithWebOptions {
|
|
10
11
|
// agentBaseOptions?: Partial<AgentOptions>;
|
|
11
12
|
// webConnectorOptions?: BrowserConnectorOptions;
|
|
@@ -29,47 +30,6 @@ export async function startBrowserAgent(options //StartAgentWithWebOptions = {}
|
|
|
29
30
|
//console.log('agent started');
|
|
30
31
|
return agent;
|
|
31
32
|
}
|
|
32
|
-
async function getFullPageContent(page) {
|
|
33
|
-
checkOperation();
|
|
34
|
-
// 1. Get all iframe element handles
|
|
35
|
-
const iframeHandles = await page.locator('iframe').elementHandles();
|
|
36
|
-
// 2. Iterate through each iframe handle
|
|
37
|
-
for (const iframeHandle of iframeHandles) {
|
|
38
|
-
checkOperation();
|
|
39
|
-
// 3. Get the Frame object for the iframe
|
|
40
|
-
const frame = await iframeHandle.contentFrame();
|
|
41
|
-
checkOperation();
|
|
42
|
-
if (frame) {
|
|
43
|
-
// 4. Get the HTML content of the iframe
|
|
44
|
-
const iframeContent = await frame.content();
|
|
45
|
-
checkOperation();
|
|
46
|
-
// 5. Use evaluate to replace the iframe element with its content.
|
|
47
|
-
// We pass the content as an argument to avoid issues with string escaping.
|
|
48
|
-
await iframeHandle.evaluate((iframeNode, { content }) => {
|
|
49
|
-
// Create a new div element to hold the iframe's content
|
|
50
|
-
const div = document.createElement('div');
|
|
51
|
-
// Use DOMParser to handle Trusted Types restrictions
|
|
52
|
-
const parser = new DOMParser();
|
|
53
|
-
const doc = parser.parseFromString(content, 'text/html');
|
|
54
|
-
// Move all body children to the div
|
|
55
|
-
while (doc.body.firstChild) {
|
|
56
|
-
div.appendChild(doc.body.firstChild);
|
|
57
|
-
}
|
|
58
|
-
// Also preserve any head elements that might be important (styles, etc)
|
|
59
|
-
const headElements = doc.head.querySelectorAll('style, link[rel="stylesheet"]');
|
|
60
|
-
headElements.forEach(el => div.appendChild(el.cloneNode(true)));
|
|
61
|
-
// Add a data-attribute to mark that this was an expanded iframe
|
|
62
|
-
div.dataset.expandedFromIframe = 'true';
|
|
63
|
-
div.dataset.iframeSrc = iframeNode.getAttribute('src') || '';
|
|
64
|
-
// Replace the iframeNode with the new div
|
|
65
|
-
iframeNode.parentNode?.replaceChild(div, iframeNode);
|
|
66
|
-
}, { content: iframeContent });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// 6. Return the final, modified page content
|
|
70
|
-
checkOperation();
|
|
71
|
-
return page.content();
|
|
72
|
-
}
|
|
73
33
|
export class BrowserAgent extends Agent {
|
|
74
34
|
browserAgentEvents = new EventEmitter();
|
|
75
35
|
constructor({ agentOptions, browserOptions }) {
|
|
@@ -98,8 +58,7 @@ export class BrowserAgent extends Agent {
|
|
|
98
58
|
}
|
|
99
59
|
async _extract(instructions, schema) {
|
|
100
60
|
this.browserAgentEvents.emit('extractStarted', instructions, schema);
|
|
101
|
-
|
|
102
|
-
const htmlContent = await retry(async () => await getFullPageContent(this.page), { retries: 5, delay: 200, exponential: true });
|
|
61
|
+
const htmlContent = await retry(() => getVisiblePageContent(this.page), { retries: 5, delay: 200, exponential: true });
|
|
103
62
|
// const accessibilityTree = await this.page.accessibility.snapshot({ interestingOnly: true });
|
|
104
63
|
// const pageRepr = renderMinimalAccessibilityTree(accessibilityTree);
|
|
105
64
|
const partitionOptions = {
|
package/dist/index.cjs
CHANGED
|
@@ -7200,6 +7200,72 @@ function narrateBrowserAgent(agent) {
|
|
|
7200
7200
|
});
|
|
7201
7201
|
}
|
|
7202
7202
|
|
|
7203
|
+
async function getVisiblePageContent(page) {
|
|
7204
|
+
return readFrame(page.mainFrame());
|
|
7205
|
+
}
|
|
7206
|
+
async function readFrame(frame) {
|
|
7207
|
+
checkOperation();
|
|
7208
|
+
const marker = crypto.randomUUID();
|
|
7209
|
+
const snapshot = await frame.evaluateHandle((marker2) => {
|
|
7210
|
+
const documentCopy = document.implementation.createHTMLDocument();
|
|
7211
|
+
const frames = [];
|
|
7212
|
+
const visibility = { checkOpacity: true, checkVisibility: true };
|
|
7213
|
+
function copy(source) {
|
|
7214
|
+
if (["script", "style", "template", "noscript"].includes(source.localName)) return null;
|
|
7215
|
+
const style = getComputedStyle(source);
|
|
7216
|
+
if (style.display === "none" || style.opacity === "0" || style.contentVisibility === "hidden") return null;
|
|
7217
|
+
const visible = style.visibility === "visible" && (style.display === "contents" || source.checkVisibility(visibility) || ["option", "optgroup"].includes(source.localName) && source.closest("select")?.checkVisibility(visibility));
|
|
7218
|
+
if (source instanceof HTMLIFrameElement || source instanceof HTMLFrameElement) {
|
|
7219
|
+
if (!visible) return null;
|
|
7220
|
+
frames.push(source);
|
|
7221
|
+
return documentCopy.createComment(`${marker2}:${frames.length - 1}`);
|
|
7222
|
+
}
|
|
7223
|
+
const target = documentCopy.importNode(source, false);
|
|
7224
|
+
if (source instanceof HTMLInputElement) {
|
|
7225
|
+
if (source.type === "password") target.removeAttribute("value");
|
|
7226
|
+
else target.setAttribute("value", source.value);
|
|
7227
|
+
target.toggleAttribute("checked", source.checked);
|
|
7228
|
+
}
|
|
7229
|
+
if (source instanceof HTMLOptionElement) target.toggleAttribute("selected", source.selected);
|
|
7230
|
+
if (source instanceof HTMLTextAreaElement) {
|
|
7231
|
+
target.textContent = visible ? source.value : "";
|
|
7232
|
+
return visible ? target : null;
|
|
7233
|
+
}
|
|
7234
|
+
const summary = source instanceof HTMLDetailsElement && !source.open ? source.querySelector("summary") : null;
|
|
7235
|
+
for (const child of source.childNodes) {
|
|
7236
|
+
if (source instanceof HTMLDetailsElement && !source.open && child !== summary) continue;
|
|
7237
|
+
if (child instanceof Element) {
|
|
7238
|
+
const copied = copy(child);
|
|
7239
|
+
if (copied) target.appendChild(copied);
|
|
7240
|
+
} else if (visible && child.nodeType === Node.TEXT_NODE) {
|
|
7241
|
+
target.appendChild(documentCopy.importNode(child, false));
|
|
7242
|
+
}
|
|
7243
|
+
}
|
|
7244
|
+
return visible || target.hasChildNodes() ? target : null;
|
|
7245
|
+
}
|
|
7246
|
+
const body = document.body && copy(document.body);
|
|
7247
|
+
return { html: body instanceof Element ? body.innerHTML : "", frames };
|
|
7248
|
+
}, marker);
|
|
7249
|
+
const handles = [snapshot];
|
|
7250
|
+
try {
|
|
7251
|
+
let html = await snapshot.evaluate(({ html: html2 }) => html2);
|
|
7252
|
+
const frames = await snapshot.getProperty("frames");
|
|
7253
|
+
handles.push(frames);
|
|
7254
|
+
const entries = await frames.getProperties();
|
|
7255
|
+
handles.push(...entries.values());
|
|
7256
|
+
for (const [index, handle] of entries) {
|
|
7257
|
+
checkOperation();
|
|
7258
|
+
const child = await handle.asElement()?.contentFrame();
|
|
7259
|
+
const content = child ? await readFrame(child) : "";
|
|
7260
|
+
html = html.replace(`<!--${marker}:${index}-->`, () => `<div>${content}</div>`);
|
|
7261
|
+
}
|
|
7262
|
+
checkOperation();
|
|
7263
|
+
return html;
|
|
7264
|
+
} finally {
|
|
7265
|
+
await Promise.all(handles.map((handle) => handle.dispose()));
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7268
|
+
|
|
7203
7269
|
async function startBrowserAgent(options) {
|
|
7204
7270
|
const { agentOptions, browserOptions } = buildDefaultBrowserAgentOptions({ agentOptions: options ?? {}, browserOptions: options ?? {} });
|
|
7205
7271
|
const agent = new BrowserAgent({
|
|
@@ -7212,34 +7278,6 @@ async function startBrowserAgent(options) {
|
|
|
7212
7278
|
await agent.start();
|
|
7213
7279
|
return agent;
|
|
7214
7280
|
}
|
|
7215
|
-
async function getFullPageContent(page) {
|
|
7216
|
-
checkOperation();
|
|
7217
|
-
const iframeHandles = await page.locator("iframe").elementHandles();
|
|
7218
|
-
for (const iframeHandle of iframeHandles) {
|
|
7219
|
-
checkOperation();
|
|
7220
|
-
const frame = await iframeHandle.contentFrame();
|
|
7221
|
-
checkOperation();
|
|
7222
|
-
if (frame) {
|
|
7223
|
-
const iframeContent = await frame.content();
|
|
7224
|
-
checkOperation();
|
|
7225
|
-
await iframeHandle.evaluate((iframeNode, { content }) => {
|
|
7226
|
-
const div = document.createElement("div");
|
|
7227
|
-
const parser = new DOMParser();
|
|
7228
|
-
const doc = parser.parseFromString(content, "text/html");
|
|
7229
|
-
while (doc.body.firstChild) {
|
|
7230
|
-
div.appendChild(doc.body.firstChild);
|
|
7231
|
-
}
|
|
7232
|
-
const headElements = doc.head.querySelectorAll('style, link[rel="stylesheet"]');
|
|
7233
|
-
headElements.forEach((el) => div.appendChild(el.cloneNode(true)));
|
|
7234
|
-
div.dataset.expandedFromIframe = "true";
|
|
7235
|
-
div.dataset.iframeSrc = iframeNode.getAttribute("src") || "";
|
|
7236
|
-
iframeNode.parentNode?.replaceChild(div, iframeNode);
|
|
7237
|
-
}, { content: iframeContent });
|
|
7238
|
-
}
|
|
7239
|
-
}
|
|
7240
|
-
checkOperation();
|
|
7241
|
-
return page.content();
|
|
7242
|
-
}
|
|
7243
7281
|
class BrowserAgent extends Agent {
|
|
7244
7282
|
browserAgentEvents = new EventEmitter();
|
|
7245
7283
|
constructor({ agentOptions, browserOptions }) {
|
|
@@ -7267,7 +7305,7 @@ class BrowserAgent extends Agent {
|
|
|
7267
7305
|
async _extract(instructions, schema) {
|
|
7268
7306
|
this.browserAgentEvents.emit("extractStarted", instructions, schema);
|
|
7269
7307
|
const htmlContent = await retry(
|
|
7270
|
-
|
|
7308
|
+
() => getVisiblePageContent(this.page),
|
|
7271
7309
|
{ retries: 5, delay: 200, exponential: true }
|
|
7272
7310
|
);
|
|
7273
7311
|
const partitionOptions = {
|
package/dist/index.mjs
CHANGED
|
@@ -7177,6 +7177,72 @@ function narrateBrowserAgent(agent) {
|
|
|
7177
7177
|
});
|
|
7178
7178
|
}
|
|
7179
7179
|
|
|
7180
|
+
async function getVisiblePageContent(page) {
|
|
7181
|
+
return readFrame(page.mainFrame());
|
|
7182
|
+
}
|
|
7183
|
+
async function readFrame(frame) {
|
|
7184
|
+
checkOperation();
|
|
7185
|
+
const marker = randomUUID();
|
|
7186
|
+
const snapshot = await frame.evaluateHandle((marker2) => {
|
|
7187
|
+
const documentCopy = document.implementation.createHTMLDocument();
|
|
7188
|
+
const frames = [];
|
|
7189
|
+
const visibility = { checkOpacity: true, checkVisibility: true };
|
|
7190
|
+
function copy(source) {
|
|
7191
|
+
if (["script", "style", "template", "noscript"].includes(source.localName)) return null;
|
|
7192
|
+
const style = getComputedStyle(source);
|
|
7193
|
+
if (style.display === "none" || style.opacity === "0" || style.contentVisibility === "hidden") return null;
|
|
7194
|
+
const visible = style.visibility === "visible" && (style.display === "contents" || source.checkVisibility(visibility) || ["option", "optgroup"].includes(source.localName) && source.closest("select")?.checkVisibility(visibility));
|
|
7195
|
+
if (source instanceof HTMLIFrameElement || source instanceof HTMLFrameElement) {
|
|
7196
|
+
if (!visible) return null;
|
|
7197
|
+
frames.push(source);
|
|
7198
|
+
return documentCopy.createComment(`${marker2}:${frames.length - 1}`);
|
|
7199
|
+
}
|
|
7200
|
+
const target = documentCopy.importNode(source, false);
|
|
7201
|
+
if (source instanceof HTMLInputElement) {
|
|
7202
|
+
if (source.type === "password") target.removeAttribute("value");
|
|
7203
|
+
else target.setAttribute("value", source.value);
|
|
7204
|
+
target.toggleAttribute("checked", source.checked);
|
|
7205
|
+
}
|
|
7206
|
+
if (source instanceof HTMLOptionElement) target.toggleAttribute("selected", source.selected);
|
|
7207
|
+
if (source instanceof HTMLTextAreaElement) {
|
|
7208
|
+
target.textContent = visible ? source.value : "";
|
|
7209
|
+
return visible ? target : null;
|
|
7210
|
+
}
|
|
7211
|
+
const summary = source instanceof HTMLDetailsElement && !source.open ? source.querySelector("summary") : null;
|
|
7212
|
+
for (const child of source.childNodes) {
|
|
7213
|
+
if (source instanceof HTMLDetailsElement && !source.open && child !== summary) continue;
|
|
7214
|
+
if (child instanceof Element) {
|
|
7215
|
+
const copied = copy(child);
|
|
7216
|
+
if (copied) target.appendChild(copied);
|
|
7217
|
+
} else if (visible && child.nodeType === Node.TEXT_NODE) {
|
|
7218
|
+
target.appendChild(documentCopy.importNode(child, false));
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
return visible || target.hasChildNodes() ? target : null;
|
|
7222
|
+
}
|
|
7223
|
+
const body = document.body && copy(document.body);
|
|
7224
|
+
return { html: body instanceof Element ? body.innerHTML : "", frames };
|
|
7225
|
+
}, marker);
|
|
7226
|
+
const handles = [snapshot];
|
|
7227
|
+
try {
|
|
7228
|
+
let html = await snapshot.evaluate(({ html: html2 }) => html2);
|
|
7229
|
+
const frames = await snapshot.getProperty("frames");
|
|
7230
|
+
handles.push(frames);
|
|
7231
|
+
const entries = await frames.getProperties();
|
|
7232
|
+
handles.push(...entries.values());
|
|
7233
|
+
for (const [index, handle] of entries) {
|
|
7234
|
+
checkOperation();
|
|
7235
|
+
const child = await handle.asElement()?.contentFrame();
|
|
7236
|
+
const content = child ? await readFrame(child) : "";
|
|
7237
|
+
html = html.replace(`<!--${marker}:${index}-->`, () => `<div>${content}</div>`);
|
|
7238
|
+
}
|
|
7239
|
+
checkOperation();
|
|
7240
|
+
return html;
|
|
7241
|
+
} finally {
|
|
7242
|
+
await Promise.all(handles.map((handle) => handle.dispose()));
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
|
|
7180
7246
|
async function startBrowserAgent(options) {
|
|
7181
7247
|
const { agentOptions, browserOptions } = buildDefaultBrowserAgentOptions({ agentOptions: options ?? {}, browserOptions: options ?? {} });
|
|
7182
7248
|
const agent = new BrowserAgent({
|
|
@@ -7189,34 +7255,6 @@ async function startBrowserAgent(options) {
|
|
|
7189
7255
|
await agent.start();
|
|
7190
7256
|
return agent;
|
|
7191
7257
|
}
|
|
7192
|
-
async function getFullPageContent(page) {
|
|
7193
|
-
checkOperation();
|
|
7194
|
-
const iframeHandles = await page.locator("iframe").elementHandles();
|
|
7195
|
-
for (const iframeHandle of iframeHandles) {
|
|
7196
|
-
checkOperation();
|
|
7197
|
-
const frame = await iframeHandle.contentFrame();
|
|
7198
|
-
checkOperation();
|
|
7199
|
-
if (frame) {
|
|
7200
|
-
const iframeContent = await frame.content();
|
|
7201
|
-
checkOperation();
|
|
7202
|
-
await iframeHandle.evaluate((iframeNode, { content }) => {
|
|
7203
|
-
const div = document.createElement("div");
|
|
7204
|
-
const parser = new DOMParser();
|
|
7205
|
-
const doc = parser.parseFromString(content, "text/html");
|
|
7206
|
-
while (doc.body.firstChild) {
|
|
7207
|
-
div.appendChild(doc.body.firstChild);
|
|
7208
|
-
}
|
|
7209
|
-
const headElements = doc.head.querySelectorAll('style, link[rel="stylesheet"]');
|
|
7210
|
-
headElements.forEach((el) => div.appendChild(el.cloneNode(true)));
|
|
7211
|
-
div.dataset.expandedFromIframe = "true";
|
|
7212
|
-
div.dataset.iframeSrc = iframeNode.getAttribute("src") || "";
|
|
7213
|
-
iframeNode.parentNode?.replaceChild(div, iframeNode);
|
|
7214
|
-
}, { content: iframeContent });
|
|
7215
|
-
}
|
|
7216
|
-
}
|
|
7217
|
-
checkOperation();
|
|
7218
|
-
return page.content();
|
|
7219
|
-
}
|
|
7220
7258
|
class BrowserAgent extends Agent {
|
|
7221
7259
|
browserAgentEvents = new EventEmitter();
|
|
7222
7260
|
constructor({ agentOptions, browserOptions }) {
|
|
@@ -7244,7 +7282,7 @@ class BrowserAgent extends Agent {
|
|
|
7244
7282
|
async _extract(instructions, schema) {
|
|
7245
7283
|
this.browserAgentEvents.emit("extractStarted", instructions, schema);
|
|
7246
7284
|
const htmlContent = await retry(
|
|
7247
|
-
|
|
7285
|
+
() => getVisiblePageContent(this.page),
|
|
7248
7286
|
{ retries: 5, delay: 200, exponential: true }
|
|
7249
7287
|
);
|
|
7250
7288
|
const partitionOptions = {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { checkOperation } from '@/common/operation';
|
|
3
|
+
export async function getVisiblePageContent(page) {
|
|
4
|
+
return readFrame(page.mainFrame());
|
|
5
|
+
}
|
|
6
|
+
async function readFrame(frame) {
|
|
7
|
+
checkOperation();
|
|
8
|
+
const marker = randomUUID();
|
|
9
|
+
const snapshot = await frame.evaluateHandle((marker) => {
|
|
10
|
+
const documentCopy = document.implementation.createHTMLDocument();
|
|
11
|
+
const frames = [];
|
|
12
|
+
const visibility = { checkOpacity: true, checkVisibility: true };
|
|
13
|
+
function copy(source) {
|
|
14
|
+
if (['script', 'style', 'template', 'noscript'].includes(source.localName))
|
|
15
|
+
return null;
|
|
16
|
+
const style = getComputedStyle(source);
|
|
17
|
+
if (style.display === 'none' || style.opacity === '0' || style.contentVisibility === 'hidden')
|
|
18
|
+
return null;
|
|
19
|
+
const visible = style.visibility === 'visible' && (style.display === 'contents' || source.checkVisibility(visibility) ||
|
|
20
|
+
(['option', 'optgroup'].includes(source.localName) && source.closest('select')?.checkVisibility(visibility)));
|
|
21
|
+
if (source instanceof HTMLIFrameElement || source instanceof HTMLFrameElement) {
|
|
22
|
+
if (!visible)
|
|
23
|
+
return null;
|
|
24
|
+
frames.push(source);
|
|
25
|
+
return documentCopy.createComment(`${marker}:${frames.length - 1}`);
|
|
26
|
+
}
|
|
27
|
+
const target = documentCopy.importNode(source, false);
|
|
28
|
+
if (source instanceof HTMLInputElement) {
|
|
29
|
+
if (source.type === 'password')
|
|
30
|
+
target.removeAttribute('value');
|
|
31
|
+
else
|
|
32
|
+
target.setAttribute('value', source.value);
|
|
33
|
+
target.toggleAttribute('checked', source.checked);
|
|
34
|
+
}
|
|
35
|
+
if (source instanceof HTMLOptionElement)
|
|
36
|
+
target.toggleAttribute('selected', source.selected);
|
|
37
|
+
if (source instanceof HTMLTextAreaElement) {
|
|
38
|
+
target.textContent = visible ? source.value : '';
|
|
39
|
+
return visible ? target : null;
|
|
40
|
+
}
|
|
41
|
+
const summary = source instanceof HTMLDetailsElement && !source.open ? source.querySelector('summary') : null;
|
|
42
|
+
for (const child of source.childNodes) {
|
|
43
|
+
if (source instanceof HTMLDetailsElement && !source.open && child !== summary)
|
|
44
|
+
continue;
|
|
45
|
+
if (child instanceof Element) {
|
|
46
|
+
const copied = copy(child);
|
|
47
|
+
if (copied)
|
|
48
|
+
target.appendChild(copied);
|
|
49
|
+
}
|
|
50
|
+
else if (visible && child.nodeType === Node.TEXT_NODE) {
|
|
51
|
+
target.appendChild(documentCopy.importNode(child, false));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return visible || target.hasChildNodes() ? target : null;
|
|
55
|
+
}
|
|
56
|
+
const body = document.body && copy(document.body);
|
|
57
|
+
return { html: body instanceof Element ? body.innerHTML : '', frames };
|
|
58
|
+
}, marker);
|
|
59
|
+
const handles = [snapshot];
|
|
60
|
+
try {
|
|
61
|
+
let html = await snapshot.evaluate(({ html }) => html);
|
|
62
|
+
const frames = await snapshot.getProperty('frames');
|
|
63
|
+
handles.push(frames);
|
|
64
|
+
const entries = await frames.getProperties();
|
|
65
|
+
handles.push(...entries.values());
|
|
66
|
+
for (const [index, handle] of entries) {
|
|
67
|
+
checkOperation();
|
|
68
|
+
const child = await handle.asElement()?.contentFrame();
|
|
69
|
+
const content = child ? await readFrame(child) : '';
|
|
70
|
+
html = html.replace(`<!--${marker}:${index}-->`, () => `<div>${content}</div>`);
|
|
71
|
+
}
|
|
72
|
+
checkOperation();
|
|
73
|
+
return html;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await Promise.all(handles.map(handle => handle.dispose()));
|
|
77
|
+
}
|
|
78
|
+
}
|