@myst-theme/jupyter 0.17.1 → 1.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/dist/BinderBadge.js +1 -1
- package/dist/ConnectionStatusTray.d.ts.map +1 -1
- package/dist/ConnectionStatusTray.js +3 -3
- package/dist/ErrorTray.d.ts.map +1 -1
- package/dist/ErrorTray.js +3 -3
- package/dist/active.d.ts +13 -0
- package/dist/active.d.ts.map +1 -0
- package/dist/active.js +67 -0
- package/dist/block.js +2 -2
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +2 -2
- package/dist/controls/Buttons.d.ts.map +1 -1
- package/dist/controls/Buttons.js +12 -12
- package/dist/controls/NotebookToolbar.js +2 -2
- package/dist/controls/Spinner.js +1 -1
- package/dist/decoration.d.ts.map +1 -1
- package/dist/decoration.js +8 -6
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.js +10 -4
- package/dist/error.js +1 -1
- package/dist/execute/utils.js +4 -4
- package/dist/figure.d.ts.map +1 -1
- package/dist/figure.js +13 -6
- package/dist/jupyter.d.ts +9 -3
- package/dist/jupyter.d.ts.map +1 -1
- package/dist/jupyter.js +26 -65
- package/dist/output.d.ts +1 -8
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +19 -45
- package/dist/output.spec.d.ts +2 -0
- package/dist/output.spec.d.ts.map +1 -0
- package/dist/output.spec.js +133 -0
- package/dist/outputs.d.ts +5 -0
- package/dist/outputs.d.ts.map +1 -0
- package/dist/outputs.js +43 -0
- package/dist/passive.d.ts +20 -0
- package/dist/passive.d.ts.map +1 -0
- package/dist/passive.js +28 -0
- package/dist/providers.d.ts +9 -0
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +11 -0
- package/dist/renderers.d.ts +2 -0
- package/dist/renderers.d.ts.map +1 -1
- package/dist/renderers.js +2 -0
- package/dist/safe.d.ts +2 -3
- package/dist/safe.d.ts.map +1 -1
- package/dist/safe.js +6 -13
- package/dist/stream.js +1 -1
- package/package.json +8 -3
package/dist/jupyter.js
CHANGED
|
@@ -1,86 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import React, { useEffect,
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
3
|
import { useFetchAnyTruncatedContent } from './hooks.js';
|
|
4
4
|
import { convertToIOutputs } from 'nbtx';
|
|
5
5
|
import { fetchAndEncodeOutputImages } from './convertImages.js';
|
|
6
6
|
import { SourceFileKind } from 'myst-spec-ext';
|
|
7
|
-
import { useXRefState } from '@myst-theme/providers';
|
|
8
7
|
import { useThebeLoader } from 'thebe-react';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const placeholder = usePlaceholder();
|
|
18
|
-
const ref = useRef(null);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
var _a, _b, _c;
|
|
21
|
-
if (!ref.current || !(exec === null || exec === void 0 ? void 0 : exec.cell)) {
|
|
22
|
-
console.debug(`Jupyter: No cell ref available for cell ${id}:${(_a = exec === null || exec === void 0 ? void 0 : exec.cell) === null || _a === void 0 ? void 0 : _a.id}`);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
const verb = exec.cell.isAttachedToDOM ? 'reattaching' : 'attaching';
|
|
26
|
-
console.debug(`${verb} cell ${exec.cell.id} to DOM at:`, {
|
|
27
|
-
el: ref.current,
|
|
28
|
-
connected: ref.current.isConnected,
|
|
29
|
-
data: (_b = core === null || core === void 0 ? void 0 : core.stripWidgets(initialData)) !== null && _b !== void 0 ? _b : initialData,
|
|
30
|
-
});
|
|
31
|
-
exec.cell.attachToDOM(ref.current);
|
|
32
|
-
if (exec.cell.executionCount == null) {
|
|
33
|
-
exec.cell.initOutputs((_c = core === null || core === void 0 ? void 0 : core.stripWidgets(initialData, true, placeholder ? () => '' : undefined)) !== null && _c !== void 0 ? _c : initialData);
|
|
34
|
-
}
|
|
35
|
-
}, [ref === null || ref === void 0 ? void 0 : ref.current, exec === null || exec === void 0 ? void 0 : exec.cell]);
|
|
36
|
-
const executed = ((_a = exec === null || exec === void 0 ? void 0 : exec.cell) === null || _a === void 0 ? void 0 : _a.executionCount) != null;
|
|
37
|
-
console.debug(`Jupyter: Cell ${id} executed: ${executed}; Show output: ${executed || !placeholder}`);
|
|
38
|
-
return (_jsxs("div", { children: [_jsx("div", { ref: ref, "data-thebe-active-ref": "true", className: classNames('relative', { 'invisible h-0': !executed && placeholder }) }), placeholder && !executed && _jsx(MyST, { ast: placeholder })] }));
|
|
39
|
-
}
|
|
40
|
-
function PassiveOutputRenderer({ id, data, core, }) {
|
|
41
|
-
const rendermime = core.makeRenderMimeRegistry();
|
|
42
|
-
const cell = useRef(new core.PassiveCellRenderer(id, rendermime, undefined));
|
|
43
|
-
const ref = useRef(null);
|
|
44
|
-
const { loaded } = usePlotlyPassively(rendermime, data);
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
var _a, _b;
|
|
47
|
-
if (!ref.current || !loaded)
|
|
48
|
-
return;
|
|
49
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
50
|
-
cell.current.attachToDOM((_a = ref.current) !== null && _a !== void 0 ? _a : undefined, true);
|
|
51
|
-
cell.current.render((_b = core === null || core === void 0 ? void 0 : core.stripWidgets(data)) !== null && _b !== void 0 ? _b : data);
|
|
52
|
-
}, [ref, loaded]);
|
|
53
|
-
return _jsx("div", { ref: ref, "data-thebe-passive-ref": "true" });
|
|
54
|
-
}
|
|
55
|
-
export const JupyterOutputs = React.memo(({ id, outputs }) => {
|
|
8
|
+
import { PassiveOutputRenderer } from './passive.js';
|
|
9
|
+
/**
|
|
10
|
+
* Render a single output as a Jupyter output.
|
|
11
|
+
*
|
|
12
|
+
* @param id - The id of the cell.
|
|
13
|
+
* @param output - The output data.
|
|
14
|
+
*/
|
|
15
|
+
export const JupyterOutput = React.memo(({ outputsId, output }) => {
|
|
56
16
|
const { core, load } = useThebeLoader();
|
|
57
|
-
const {
|
|
58
|
-
const
|
|
59
|
-
const [fullOutputs, setFullOutputs] = useState(null);
|
|
60
|
-
const exec = useCellExecution(id);
|
|
61
|
-
const placeholder = usePlaceholder();
|
|
17
|
+
const { data, error } = useFetchAnyTruncatedContent([output]);
|
|
18
|
+
const [fullOutput, setFullOutput] = useState(null);
|
|
62
19
|
useEffect(() => {
|
|
63
20
|
if (core)
|
|
64
21
|
return;
|
|
65
22
|
load();
|
|
66
23
|
}, [core, load]);
|
|
67
24
|
useEffect(() => {
|
|
68
|
-
if (!data ||
|
|
25
|
+
if (!data || fullOutput != null)
|
|
69
26
|
return;
|
|
70
27
|
fetchAndEncodeOutputImages(data).then((out) => {
|
|
71
28
|
const compactOutputs = convertToIOutputs(out, {});
|
|
72
|
-
|
|
29
|
+
setFullOutput(compactOutputs[0]);
|
|
73
30
|
});
|
|
74
|
-
}, [
|
|
31
|
+
}, [outputsId, data, fullOutput]);
|
|
75
32
|
if (error) {
|
|
76
33
|
console.error(error);
|
|
77
34
|
return _jsxs("div", { className: "text-red-500", children: ["Error rendering output: ", error.message] });
|
|
78
35
|
}
|
|
79
|
-
if (!inCrossRef &&
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
36
|
+
// if (!inCrossRef && exec?.ready) {
|
|
37
|
+
// return (
|
|
38
|
+
// <div>
|
|
39
|
+
// {!fullOutputs && <div className="p-2.5">Fetching full output data...</div>}
|
|
40
|
+
// {core && fullOutputs && (
|
|
41
|
+
// <ActiveOutputRenderer key={id} id={id} initialData={fullOutputs} core={core} />
|
|
42
|
+
// )}
|
|
43
|
+
// </div>
|
|
44
|
+
// );
|
|
45
|
+
// }
|
|
46
|
+
return (_jsxs("div", { children: [!fullOutput && _jsx("div", { className: "p-2.5", children: "Loading..." }), fullOutput && core && (_jsx(PassiveOutputRenderer, { id: outputsId, data: fullOutput, core: core, kind: SourceFileKind.Notebook }))] }));
|
|
86
47
|
});
|
package/dist/output.d.ts
CHANGED
|
@@ -2,14 +2,7 @@ import type { GenericNode } from 'myst-common';
|
|
|
2
2
|
import type { MinifiedOutput } from 'nbtx';
|
|
3
3
|
export declare const DIRECT_OUTPUT_TYPES: Set<string>;
|
|
4
4
|
export declare const DIRECT_MIME_TYPES: Set<string>;
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function JupyterOutput({ outputId, identifier, data, align, className, }: {
|
|
7
|
-
outputId: string;
|
|
8
|
-
identifier?: string;
|
|
9
|
-
data: MinifiedOutput[];
|
|
10
|
-
align?: 'left' | 'center' | 'right';
|
|
11
|
-
className?: string;
|
|
12
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export declare function isOutputSafe(output: MinifiedOutput, directOutputTypes: Set<string>, directMimeTypes: Set<string>): boolean;
|
|
13
6
|
export declare function Output({ node }: {
|
|
14
7
|
node: GenericNode;
|
|
15
8
|
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/output.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAsB,cAAc,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAsB,cAAc,EAAE,MAAM,MAAM,CAAC;AAO/D,eAAO,MAAM,mBAAmB,aAA+B,CAAC;AAEhE,eAAO,MAAM,iBAAiB,EAMxB,GAAG,CAAC,MAAM,CAAC,CAAC;AAElB,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,EAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,WAU7B;AAED,wBAAgB,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,2CAYrD"}
|
package/dist/output.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { KnownCellOutputMimeTypes } from 'nbtx';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { JupyterOutputs } from './jupyter.js';
|
|
3
|
+
import { SafeOutput } from './safe.js';
|
|
4
|
+
import { JupyterOutput } from './jupyter.js';
|
|
6
5
|
import { useMemo } from 'react';
|
|
7
6
|
import { useCellExecution } from './execute/index.js';
|
|
8
|
-
import {
|
|
9
|
-
import { Details, MyST } from 'myst-to-react';
|
|
7
|
+
import { useOutputsContext } from './providers.js';
|
|
10
8
|
export const DIRECT_OUTPUT_TYPES = new Set(['stream', 'error']);
|
|
11
9
|
export const DIRECT_MIME_TYPES = new Set([
|
|
12
10
|
KnownCellOutputMimeTypes.TextPlain,
|
|
@@ -15,48 +13,24 @@ export const DIRECT_MIME_TYPES = new Set([
|
|
|
15
13
|
KnownCellOutputMimeTypes.ImageJpeg,
|
|
16
14
|
KnownCellOutputMimeTypes.ImageBmp,
|
|
17
15
|
]);
|
|
18
|
-
export function
|
|
19
|
-
if (
|
|
16
|
+
export function isOutputSafe(output, directOutputTypes, directMimeTypes) {
|
|
17
|
+
if (directOutputTypes.has(output.output_type))
|
|
20
18
|
return true;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const safe = 'data' in output &&
|
|
27
|
-
Boolean(output.data) &&
|
|
28
|
-
mimetypes.every((mimetype) => directMimeTypes.has(mimetype));
|
|
29
|
-
return flag && safe;
|
|
30
|
-
}, true);
|
|
31
|
-
}
|
|
32
|
-
export function JupyterOutput({ outputId, identifier, data, align, className, }) {
|
|
33
|
-
const { ready } = useCellExecution(outputId);
|
|
34
|
-
const outputs = data;
|
|
35
|
-
const allSafe = useMemo(() => allOutputsAreSafe(outputs, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES), [outputs]);
|
|
36
|
-
const placeholder = usePlaceholder();
|
|
37
|
-
let component;
|
|
38
|
-
if (allSafe && !ready) {
|
|
39
|
-
if (placeholder && (!outputs || outputs.length === 0)) {
|
|
40
|
-
if (placeholder) {
|
|
41
|
-
return _jsx(MyST, { ast: placeholder });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
component = _jsx(SafeOutputs, { keyStub: outputId, outputs: outputs });
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
component = _jsx(JupyterOutputs, { id: outputId, outputs: outputs });
|
|
48
|
-
}
|
|
49
|
-
return (_jsx("div", { id: identifier || undefined, "data-mdast-node-id": outputId, className: classNames('max-w-full overflow-y-visible overflow-x-auto m-0 group not-prose relative', {
|
|
50
|
-
'text-left': !align || align === 'left',
|
|
51
|
-
'text-center': align === 'center',
|
|
52
|
-
'text-right': align === 'right',
|
|
53
|
-
'mb-5': outputs && outputs.length > 0,
|
|
54
|
-
}, className), children: component }));
|
|
19
|
+
const data = output.data;
|
|
20
|
+
const mimetypes = data ? Object.keys(data) : [];
|
|
21
|
+
return ('data' in output &&
|
|
22
|
+
Boolean(output.data) &&
|
|
23
|
+
mimetypes.every((mimetype) => directMimeTypes.has(mimetype)));
|
|
55
24
|
}
|
|
56
25
|
export function Output({ node }) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
26
|
+
const { outputsId } = useOutputsContext();
|
|
27
|
+
const { ready } = useCellExecution(outputsId);
|
|
28
|
+
// FUTURE: we'll be rendering AST outputs directly in future
|
|
29
|
+
const maybeSafeOutput = useMemo(() => node.jupyter_data, [node]);
|
|
30
|
+
const isSafe = isOutputSafe(maybeSafeOutput, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES);
|
|
31
|
+
if (isSafe && !ready) {
|
|
32
|
+
return _jsx(SafeOutput, { output: maybeSafeOutput });
|
|
60
33
|
}
|
|
61
|
-
|
|
34
|
+
// TODO: myst-jp-output should be added to the first child div
|
|
35
|
+
return _jsx(JupyterOutput, { outputsId: outputsId, output: maybeSafeOutput });
|
|
62
36
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.spec.d.ts","sourceRoot":"","sources":["../src/output.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { isOutputSafe, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES } from './output.js';
|
|
4
|
+
import { KnownCellOutputMimeTypes } from 'nbtx';
|
|
5
|
+
describe('Output Safety Functions', () => {
|
|
6
|
+
describe('isOutputSafe', () => {
|
|
7
|
+
it('should return true for direct output types', () => {
|
|
8
|
+
const output = {
|
|
9
|
+
output_type: 'stream',
|
|
10
|
+
name: 'stdout',
|
|
11
|
+
text: 'some output',
|
|
12
|
+
};
|
|
13
|
+
expect(isOutputSafe(output, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('should return true for safe mime types', () => {
|
|
16
|
+
const output = {
|
|
17
|
+
output_type: 'display_data',
|
|
18
|
+
data: {
|
|
19
|
+
[KnownCellOutputMimeTypes.TextPlain]: {
|
|
20
|
+
content_type: 'text/plain',
|
|
21
|
+
content: 'some text',
|
|
22
|
+
},
|
|
23
|
+
[KnownCellOutputMimeTypes.ImagePng]: {
|
|
24
|
+
content_type: 'image/png',
|
|
25
|
+
content: 'base64data',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
metadata: {},
|
|
29
|
+
};
|
|
30
|
+
expect(isOutputSafe(output, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('should return false for unsafe mime types', () => {
|
|
33
|
+
const output = {
|
|
34
|
+
output_type: 'display_data',
|
|
35
|
+
data: {
|
|
36
|
+
'application/javascript': {
|
|
37
|
+
content_type: 'application/javascript',
|
|
38
|
+
content: 'alert("unsafe")',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
metadata: {},
|
|
42
|
+
};
|
|
43
|
+
expect(isOutputSafe(output, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
it('should return false for output without data', () => {
|
|
46
|
+
// technically malformed input, but we'll handle it gracefully
|
|
47
|
+
const output = {
|
|
48
|
+
output_type: 'display_data',
|
|
49
|
+
metadata: {},
|
|
50
|
+
};
|
|
51
|
+
expect(isOutputSafe(output, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
// describe('allOutputsAreSafe', () => {
|
|
55
|
+
// it('should return true for empty outputs', () => {
|
|
56
|
+
// expect(allOutputsAreSafe([], DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES)).toBe(true);
|
|
57
|
+
// });
|
|
58
|
+
// it('should return true when all outputs are safe', () => {
|
|
59
|
+
// const outputs: MinifiedOutput[] = [
|
|
60
|
+
// {
|
|
61
|
+
// output_type: 'stream',
|
|
62
|
+
// name: 'stdout',
|
|
63
|
+
// text: 'some output',
|
|
64
|
+
// },
|
|
65
|
+
// {
|
|
66
|
+
// output_type: 'display_data',
|
|
67
|
+
// data: {
|
|
68
|
+
// [KnownCellOutputMimeTypes.TextPlain]: {
|
|
69
|
+
// content_type: 'text/plain',
|
|
70
|
+
// content: 'text',
|
|
71
|
+
// },
|
|
72
|
+
// [KnownCellOutputMimeTypes.ImagePng]: {
|
|
73
|
+
// content_type: 'image/png',
|
|
74
|
+
// content: 'data',
|
|
75
|
+
// },
|
|
76
|
+
// },
|
|
77
|
+
// metadata: {},
|
|
78
|
+
// },
|
|
79
|
+
// ];
|
|
80
|
+
// expect(allOutputsAreSafe(outputs, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES)).toBe(true);
|
|
81
|
+
// });
|
|
82
|
+
// it('should return false when any output is unsafe', () => {
|
|
83
|
+
// const outputs: MinifiedOutput[] = [
|
|
84
|
+
// {
|
|
85
|
+
// output_type: 'stream',
|
|
86
|
+
// name: 'stdout',
|
|
87
|
+
// text: 'some output',
|
|
88
|
+
// },
|
|
89
|
+
// {
|
|
90
|
+
// output_type: 'display_data',
|
|
91
|
+
// data: {
|
|
92
|
+
// 'application/javascript': {
|
|
93
|
+
// content_type: 'application/javascript',
|
|
94
|
+
// content: 'alert("unsafe")',
|
|
95
|
+
// },
|
|
96
|
+
// },
|
|
97
|
+
// metadata: {},
|
|
98
|
+
// },
|
|
99
|
+
// ];
|
|
100
|
+
// expect(allOutputsAreSafe(outputs, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES)).toBe(false);
|
|
101
|
+
// });
|
|
102
|
+
// it('should handle mixed safe and unsafe outputs', () => {
|
|
103
|
+
// const outputs: MinifiedOutput[] = [
|
|
104
|
+
// {
|
|
105
|
+
// output_type: 'stream',
|
|
106
|
+
// name: 'stdout',
|
|
107
|
+
// text: 'some output',
|
|
108
|
+
// },
|
|
109
|
+
// {
|
|
110
|
+
// output_type: 'display_data',
|
|
111
|
+
// data: {
|
|
112
|
+
// [KnownCellOutputMimeTypes.TextPlain]: {
|
|
113
|
+
// content_type: 'text/plain',
|
|
114
|
+
// content: 'text',
|
|
115
|
+
// },
|
|
116
|
+
// },
|
|
117
|
+
// metadata: {},
|
|
118
|
+
// },
|
|
119
|
+
// {
|
|
120
|
+
// output_type: 'display_data',
|
|
121
|
+
// data: {
|
|
122
|
+
// 'application/javascript': {
|
|
123
|
+
// content_type: 'application/javascript',
|
|
124
|
+
// content: 'alert("unsafe")',
|
|
125
|
+
// },
|
|
126
|
+
// },
|
|
127
|
+
// metadata: {},
|
|
128
|
+
// },
|
|
129
|
+
// ];
|
|
130
|
+
// expect(allOutputsAreSafe(outputs, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES)).toBe(false);
|
|
131
|
+
// });
|
|
132
|
+
// });
|
|
133
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outputs.d.ts","sourceRoot":"","sources":["../src/outputs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAU/C,wBAAgB,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,2CAoDtD"}
|
package/dist/outputs.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useCellExecution } from './execute/index.js';
|
|
5
|
+
import { usePlaceholder } from './decoration.js';
|
|
6
|
+
import { Details, MyST } from 'myst-to-react';
|
|
7
|
+
import { selectAll } from 'unist-util-select';
|
|
8
|
+
import { OutputsContextProvider } from './providers.js';
|
|
9
|
+
import { ActiveJupyterCellOutputs } from './active.js';
|
|
10
|
+
export function Outputs({ node }) {
|
|
11
|
+
var _a;
|
|
12
|
+
const className = classNames({ hidden: node.visibility === 'remove' });
|
|
13
|
+
const { children, identifier, align } = node;
|
|
14
|
+
const outputsId = (_a = node.id) !== null && _a !== void 0 ? _a : node.key;
|
|
15
|
+
const cellExecutionContext = useCellExecution(outputsId);
|
|
16
|
+
const { ready } = cellExecutionContext;
|
|
17
|
+
const legacyOutputsArray = useMemo(() => {
|
|
18
|
+
return selectAll('output', node).map((child) => child.jupyter_data);
|
|
19
|
+
}, [children]);
|
|
20
|
+
const atLeastOneChildHasJupyterData = legacyOutputsArray.length > 0;
|
|
21
|
+
// NOTE: a placeholder is actually an option on the figure node, not the outputs node
|
|
22
|
+
// perhaps we should move the placeholder to the figure renderer?
|
|
23
|
+
const placeholder = usePlaceholder();
|
|
24
|
+
if (!ready && placeholder) {
|
|
25
|
+
return _jsx(MyST, { ast: placeholder });
|
|
26
|
+
}
|
|
27
|
+
// if not ready, we leave each output to independently render
|
|
28
|
+
// NOTE: we need to see that bokeh & plotly can render ok in this way
|
|
29
|
+
if (!ready) {
|
|
30
|
+
const passiveOutputs = (_jsx("div", { "data-name": "outputs-container", id: identifier || undefined, "data-mdast-node-id": outputsId, className: classNames('max-w-full overflow-y-visible overflow-x-auto m-0 group not-prose relative', {
|
|
31
|
+
'text-left': !align || align === 'left',
|
|
32
|
+
'text-center': align === 'center',
|
|
33
|
+
'text-right': align === 'right',
|
|
34
|
+
'mb-5': atLeastOneChildHasJupyterData,
|
|
35
|
+
}, className), children: _jsx(OutputsContextProvider, { outputsId: outputsId, children: _jsx(MyST, { ast: children }) }) }));
|
|
36
|
+
if (node.visibility === 'hide') {
|
|
37
|
+
return _jsx(Details, { title: "Output", children: passiveOutputs });
|
|
38
|
+
}
|
|
39
|
+
return passiveOutputs;
|
|
40
|
+
}
|
|
41
|
+
// else compute is ready, and we need to treat the whole cell output as one unit
|
|
42
|
+
return _jsx(ActiveJupyterCellOutputs, { outputsId: outputsId, outputs: legacyOutputsArray });
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IOutput } from '@jupyterlab/nbformat';
|
|
2
|
+
import type { ThebeCore } from 'thebe-core';
|
|
3
|
+
import type { SourceFileKind } from 'myst-spec-ext';
|
|
4
|
+
/**
|
|
5
|
+
* Render a single output as a passive cell output.
|
|
6
|
+
*
|
|
7
|
+
* This is used for outputs that require jupyters rendermime support, such as Plotly.
|
|
8
|
+
*
|
|
9
|
+
* @param id - The id of the cell.
|
|
10
|
+
* @param data - The output data.
|
|
11
|
+
* @param core - The Thebe core.
|
|
12
|
+
* @param kind - The kind of source file.
|
|
13
|
+
*/
|
|
14
|
+
export declare function PassiveOutputRenderer({ id, data, core, }: {
|
|
15
|
+
id: string;
|
|
16
|
+
data: IOutput;
|
|
17
|
+
core: ThebeCore;
|
|
18
|
+
kind: SourceFileKind;
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=passive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passive.d.ts","sourceRoot":"","sources":["../src/passive.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,EAAE,EACF,IAAI,EACJ,IAAI,GACL,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,cAAc,CAAC;CACtB,2CAgBA"}
|
package/dist/passive.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { usePlotlyPassively } from './plotly.js';
|
|
4
|
+
/**
|
|
5
|
+
* Render a single output as a passive cell output.
|
|
6
|
+
*
|
|
7
|
+
* This is used for outputs that require jupyters rendermime support, such as Plotly.
|
|
8
|
+
*
|
|
9
|
+
* @param id - The id of the cell.
|
|
10
|
+
* @param data - The output data.
|
|
11
|
+
* @param core - The Thebe core.
|
|
12
|
+
* @param kind - The kind of source file.
|
|
13
|
+
*/
|
|
14
|
+
export function PassiveOutputRenderer({ id, data, core, }) {
|
|
15
|
+
const rendermime = core.makeRenderMimeRegistry();
|
|
16
|
+
const cell = useRef(new core.PassiveCellRenderer(id, rendermime, undefined));
|
|
17
|
+
const ref = useRef(null);
|
|
18
|
+
const { loaded } = usePlotlyPassively(rendermime, [data]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
var _a, _b;
|
|
21
|
+
if (!ref.current || !loaded)
|
|
22
|
+
return;
|
|
23
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
24
|
+
cell.current.attachToDOM((_a = ref.current) !== null && _a !== void 0 ? _a : undefined, true);
|
|
25
|
+
cell.current.render((_b = core === null || core === void 0 ? void 0 : core.stripWidgets([data])) !== null && _b !== void 0 ? _b : data);
|
|
26
|
+
}, [ref, loaded]);
|
|
27
|
+
return _jsx("div", { ref: ref, "data-thebe-passive-ref": "true", "data-output-id": id });
|
|
28
|
+
}
|
package/dist/providers.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { type ExtendedCoreOptions } from './utils.js';
|
|
4
4
|
import type { GenericParent } from 'myst-common';
|
|
5
5
|
import type { RepoProviderSpec } from 'thebe-core';
|
|
6
|
+
import type { IdOrKey } from './execute/types.js';
|
|
6
7
|
type ComputeOptionsContextType = {
|
|
7
8
|
enabled: boolean;
|
|
8
9
|
features: {
|
|
@@ -41,5 +42,13 @@ export declare function ThebeLoaderAndServer({ baseurl, connect, children, }: Re
|
|
|
41
42
|
connect?: boolean;
|
|
42
43
|
baseurl?: string;
|
|
43
44
|
}>): import("react/jsx-runtime").JSX.Element;
|
|
45
|
+
type OutputsContextType = {
|
|
46
|
+
outputsId: IdOrKey;
|
|
47
|
+
};
|
|
48
|
+
export declare function useOutputsContext(): OutputsContextType;
|
|
49
|
+
export declare function OutputsContextProvider({ outputsId, children, }: {
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
outputsId: IdOrKey;
|
|
52
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
44
53
|
export {};
|
|
45
54
|
//# sourceMappingURL=providers.d.ts.map
|
package/dist/providers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../src/providers.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,KAAK,mBAAmB,EAA6B,MAAM,YAAY,CAAC;AACjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../src/providers.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,KAAK,mBAAmB,EAA6B,MAAM,YAAY,CAAC;AACjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,KAAK,yBAAyB,GAAG;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE;QACR,eAAe,EAAE,OAAO,CAAC;QACzB,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC;IACF,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,mBAAmB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC1C,CAAC;AAIF,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,gBAAgB,EAChB,mBAAmB,EACnB,QAAQ,GACT,EAAE,KAAK,CAAC,iBAAiB,CAAC;IACzB,QAAQ,EAAE;QACR,eAAe,EAAE,OAAO,CAAC;QACzB,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC;IACF,gBAAgB,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,mBAAmB,KAAK,mBAAmB,GAAG,SAAS,CAAC;IACnF,mBAAmB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC1C,CAAC,2CA2BD;AAED,wBAAgB,aAAa,wBAG5B;AAED,wBAAgB,iBAAiB,0CAEhC;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,aAAa,CAAC;CACtB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,OAAO,EACP,OAAO,EACP,QAAQ,GACT,EAAE,KAAK,CAAC,iBAAiB,CAAC;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,2CAkBlE;AAED,KAAK,kBAAkB,GAAG;IACxB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAGF,wBAAgB,iBAAiB,uBAMhC;AACD,wBAAgB,sBAAsB,CAAC,EACrC,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB,2CAEA"}
|
package/dist/providers.js
CHANGED
|
@@ -44,3 +44,14 @@ export function ThebeLoaderAndServer({ baseurl, connect, children, }) {
|
|
|
44
44
|
const compute = useComputeOptions();
|
|
45
45
|
return (_jsx(ThebeBundleLoaderProvider, { loadThebeLite: (_b = (_a = compute === null || compute === void 0 ? void 0 : compute.thebe) === null || _a === void 0 ? void 0 : _a.useJupyterLite) !== null && _b !== void 0 ? _b : false, publicPath: baseurl, children: _jsx(ThebeServerProvider, { connect: connect !== null && connect !== void 0 ? connect : false, options: compute === null || compute === void 0 ? void 0 : compute.thebe, useBinder: (_d = (_c = compute === null || compute === void 0 ? void 0 : compute.thebe) === null || _c === void 0 ? void 0 : _c.useBinder) !== null && _d !== void 0 ? _d : false, useJupyterLite: (_f = (_e = compute === null || compute === void 0 ? void 0 : compute.thebe) === null || _e === void 0 ? void 0 : _e.useJupyterLite) !== null && _f !== void 0 ? _f : false, customRepoProviders: (_g = compute === null || compute === void 0 ? void 0 : compute.customRepoProviders) !== null && _g !== void 0 ? _g : [], children: children }) }));
|
|
46
46
|
}
|
|
47
|
+
const OutputsContext = React.createContext(null);
|
|
48
|
+
export function useOutputsContext() {
|
|
49
|
+
const context = useContext(OutputsContext);
|
|
50
|
+
if (context === null) {
|
|
51
|
+
throw new Error('useOutputsContext must be used within a OutputsContextProvider');
|
|
52
|
+
}
|
|
53
|
+
return context;
|
|
54
|
+
}
|
|
55
|
+
export function OutputsContextProvider({ outputsId, children, }) {
|
|
56
|
+
return _jsx(OutputsContext.Provider, { value: { outputsId }, children: children });
|
|
57
|
+
}
|
package/dist/renderers.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Embed } from './embed.js';
|
|
2
|
+
import { Outputs } from './outputs.js';
|
|
2
3
|
import { Output } from './output.js';
|
|
3
4
|
import { Figure } from './figure.js';
|
|
4
5
|
export { NOTEBOOK_BLOCK_RENDERERS } from './block.js';
|
|
5
6
|
export declare const OUTPUT_RENDERERS: {
|
|
7
|
+
outputs: typeof Outputs;
|
|
6
8
|
output: typeof Output;
|
|
7
9
|
embed: typeof Embed;
|
|
8
10
|
container: typeof Figure;
|
package/dist/renderers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderers.d.ts","sourceRoot":"","sources":["../src/renderers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIrC,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACtD,eAAO,MAAM,gBAAgB
|
|
1
|
+
{"version":3,"file":"renderers.d.ts","sourceRoot":"","sources":["../src/renderers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIrC,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACtD,eAAO,MAAM,gBAAgB;;;;;CAK5B,CAAC;AACF,eAAO,MAAM,iBAAiB,+CAA+D,CAAC"}
|
package/dist/renderers.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Embed } from './embed.js';
|
|
2
|
+
import { Outputs } from './outputs.js';
|
|
2
3
|
import { Output } from './output.js';
|
|
3
4
|
import { Figure } from './figure.js';
|
|
4
5
|
import { mergeRenderers } from '@myst-theme/providers';
|
|
5
6
|
import { NOTEBOOK_BLOCK_RENDERERS } from './block.js';
|
|
6
7
|
export { NOTEBOOK_BLOCK_RENDERERS } from './block.js';
|
|
7
8
|
export const OUTPUT_RENDERERS = {
|
|
9
|
+
outputs: Outputs,
|
|
8
10
|
output: Output,
|
|
9
11
|
embed: Embed,
|
|
10
12
|
container: Figure,
|
package/dist/safe.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { MinifiedOutput } from 'nbtx';
|
|
2
|
-
export declare function
|
|
3
|
-
|
|
4
|
-
outputs: MinifiedOutput[];
|
|
2
|
+
export declare function SafeOutput({ output }: {
|
|
3
|
+
output: MinifiedOutput;
|
|
5
4
|
}): import("react/jsx-runtime").JSX.Element | null;
|
|
6
5
|
//# sourceMappingURL=safe.d.ts.map
|
package/dist/safe.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA2C,cAAc,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA2C,cAAc,EAAE,MAAM,MAAM,CAAC;AA8CpF,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,kDAwChE"}
|
package/dist/safe.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { KnownCellOutputMimeTypes } from 'nbtx';
|
|
3
3
|
import Stream from './stream.js';
|
|
4
4
|
import Error from './error.js';
|
|
@@ -40,12 +40,12 @@ function OutputImage({ image, text }) {
|
|
|
40
40
|
var _a;
|
|
41
41
|
return _jsx("img", { src: image === null || image === void 0 ? void 0 : image.path, alt: (_a = text === null || text === void 0 ? void 0 : text.content) !== null && _a !== void 0 ? _a : 'Image produced in Jupyter' });
|
|
42
42
|
}
|
|
43
|
-
function SafeOutput({ output }) {
|
|
43
|
+
export function SafeOutput({ output }) {
|
|
44
44
|
switch (output.output_type) {
|
|
45
45
|
case 'stream':
|
|
46
|
-
return _jsx(Stream, { output: output });
|
|
46
|
+
return (_jsx("div", { "data-name": "safe-output-stream", children: _jsx(Stream, { output: output }) }));
|
|
47
47
|
case 'error':
|
|
48
|
-
return _jsx(Error, { output: output });
|
|
48
|
+
return (_jsx("div", { "data-name": "safe-output-error", children: _jsx(Error, { output: output }) }));
|
|
49
49
|
case 'display_data':
|
|
50
50
|
case 'execute_result':
|
|
51
51
|
case 'update_display_data': {
|
|
@@ -53,9 +53,9 @@ function SafeOutput({ output }) {
|
|
|
53
53
|
if (!image && !text)
|
|
54
54
|
return null;
|
|
55
55
|
if (image)
|
|
56
|
-
return _jsx(OutputImage, { image: image, text: text });
|
|
56
|
+
return (_jsx("div", { "data-name": "safe-output-image", children: _jsx(OutputImage, { image: image, text: text }) }));
|
|
57
57
|
if (text)
|
|
58
|
-
return (_jsx("div", { className: "font-mono text-sm whitespace-pre-wrap", children: _jsx(Ansi, { children: text.content }) }));
|
|
58
|
+
return (_jsx("div", { "data-name": "safe-output-text", className: "font-mono text-sm whitespace-pre-wrap myst-jp-safe-output-text", children: _jsx(Ansi, { children: text.content }) }));
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
default:
|
|
@@ -63,10 +63,3 @@ function SafeOutput({ output }) {
|
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
export function SafeOutputs({ keyStub, outputs }) {
|
|
67
|
-
if (!outputs)
|
|
68
|
-
return null;
|
|
69
|
-
// TODO better key - add keys during content creation?
|
|
70
|
-
const components = outputs.map((output, idx) => (_jsx(SafeOutput, { output: output }, `${keyStub}-${idx}`)));
|
|
71
|
-
return _jsx(_Fragment, { children: components });
|
|
72
|
-
}
|
package/dist/stream.js
CHANGED
|
@@ -3,5 +3,5 @@ import Ansi from '@curvenote/ansi-to-react';
|
|
|
3
3
|
import { ensureString } from 'nbtx';
|
|
4
4
|
import { MaybeLongContent } from './components.js';
|
|
5
5
|
export default function Stream({ output }) {
|
|
6
|
-
return (_jsx(MaybeLongContent, { content: ensureString(output.text), path: output.path, render: (content) => (_jsx("pre", { className: "text-sm font-thin font-system", children: _jsx(Ansi, { children: content !== null && content !== void 0 ? content : '' }) })) }));
|
|
6
|
+
return (_jsx(MaybeLongContent, { content: ensureString(output.text), path: output.path, render: (content) => (_jsx("pre", { className: "myst-jp-stream-output text-sm font-thin font-system", children: _jsx(Ansi, { children: content !== null && content !== void 0 ? content : '' }) })) }));
|
|
7
7
|
}
|