@lincyaw/addon-exec 0.0.0-stage → 1.0.0-rc.10
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/LICENSE +202 -0
- package/LICENSES +696 -0
- package/README.md +33 -2
- package/components/PmRunPanel.css +167 -0
- package/components/PmRunPanel.tsx +389 -0
- package/components/PmStepRun.css +61 -0
- package/components/PmStepRun.tsx +202 -0
- package/package.json +50 -3
- package/src/client/RunOutputs.css +21 -0
- package/src/client/RunOutputs.tsx +69 -0
- package/src/client/StepMark.css +3 -0
- package/src/client/StepMark.tsx +12 -0
- package/src/client/env.d.ts +13 -0
- package/src/client/feedback.ts +69 -0
- package/src/client/format.ts +30 -0
- package/src/client/index.ts +14 -0
- package/src/client/messages.ts +102 -0
- package/src/client/outputs.ts +29 -0
- package/src/client/plugin.ts +55 -0
- package/src/client/pyodide/executor.ts +94 -0
- package/src/client/pyodide/presmium_gpu.py +297 -0
- package/src/client/pyodide/protocol.ts +27 -0
- package/src/client/pyodide/pyodide.worker.ts +68 -0
- package/src/client/pyodide/run_await.py +57 -0
- package/src/client/runs-in.ts +27 -0
- package/src/client/script/executor.ts +62 -0
- package/src/client/script/script.worker.ts +9 -0
- package/src/client/slide-fit.ts +61 -0
- package/src/client/step-marks.ts +7 -0
- package/src/client/when-service.ts +14 -0
- package/src/domain/browser-python.ts +33 -0
- package/src/domain/config.ts +41 -0
- package/src/domain/index.ts +11 -0
- package/src/domain/outputs.ts +18 -0
- package/src/domain/router.ts +50 -0
- package/src/domain/runs.ts +72 -0
- package/src/domain/static-output.ts +20 -0
- package/src/domain/trace.ts +32 -0
- package/src/node/browser-imports.ts +34 -0
- package/src/node/child-process.ts +109 -0
- package/src/node/code-component.ts +48 -0
- package/src/node/notebook-cells.ts +46 -0
- package/src/node/presmium.ts +27 -0
- package/src/node/pyodide.ts +22 -0
- package/src/node/python.ts +40 -0
- package/src/node/run-panel.ts +71 -0
- package/src/node/script-main.mjs +11 -0
- package/src/node/script.ts +26 -0
- package/src/node/step-run.ts +71 -0
- package/src/node/timeouts.ts +15 -0
- package/src/shared/script-runner.d.mts +16 -0
- package/src/shared/script-runner.mjs +168 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { errorMessage } from '@lincyaw/kernel'
|
|
4
|
+
import {
|
|
5
|
+
PmIconButton,
|
|
6
|
+
useStep,
|
|
7
|
+
useStepScope,
|
|
8
|
+
useMessages,
|
|
9
|
+
usePresentationMode,
|
|
10
|
+
useSectionConfig,
|
|
11
|
+
useService,
|
|
12
|
+
useStartReport,
|
|
13
|
+
useViewRole,
|
|
14
|
+
} from '@lincyaw/host'
|
|
15
|
+
import { markLine, MESSAGES } from '../src/client/index.ts'
|
|
16
|
+
import {
|
|
17
|
+
configuredTimeout,
|
|
18
|
+
EXEC_ID,
|
|
19
|
+
frameAt,
|
|
20
|
+
stringArgs,
|
|
21
|
+
TRACER,
|
|
22
|
+
} from '../src/domain/index.ts'
|
|
23
|
+
import type { Trace } from '../src/domain/index.ts'
|
|
24
|
+
import './PmStepRun.css'
|
|
25
|
+
export default function PmStepRun({
|
|
26
|
+
lang = 'python',
|
|
27
|
+
code = '',
|
|
28
|
+
params,
|
|
29
|
+
timeout,
|
|
30
|
+
steps,
|
|
31
|
+
children,
|
|
32
|
+
}: {
|
|
33
|
+
lang?: string
|
|
34
|
+
code?: string
|
|
35
|
+
params?: Record<string, string | number | boolean>
|
|
36
|
+
timeout?: number
|
|
37
|
+
steps?: number
|
|
38
|
+
children?: ReactNode
|
|
39
|
+
}) {
|
|
40
|
+
const tracer = useService(TRACER),
|
|
41
|
+
report = useStartReport(),
|
|
42
|
+
config = useSectionConfig(EXEC_ID),
|
|
43
|
+
t = useMessages(MESSAGES)
|
|
44
|
+
const root = useRef<HTMLDivElement>(null),
|
|
45
|
+
source = useRef<HTMLDivElement>(null)
|
|
46
|
+
const [near, setNear] = useState(false),
|
|
47
|
+
[trace, setTrace] = useState<Trace>(),
|
|
48
|
+
[local, setLocal] = useState<number>()
|
|
49
|
+
const hostStep = useStep(),
|
|
50
|
+
scope = useStepScope(),
|
|
51
|
+
role = useViewRole()
|
|
52
|
+
const expanded = !Number.isFinite(hostStep),
|
|
53
|
+
lazy = usePresentationMode() === 'document'
|
|
54
|
+
const count = steps ?? trace?.events.length ?? 0
|
|
55
|
+
const traced = trace?.error === undefined ? trace?.events.length : undefined
|
|
56
|
+
const last = traced ?? count
|
|
57
|
+
const sectionStep = Math.max(0, Math.min(count, scope.localStepAt(hostStep)))
|
|
58
|
+
const step = expanded ? (local ?? last) : sectionStep
|
|
59
|
+
const frame = trace ? frameAt(trace, step) : undefined
|
|
60
|
+
const state = trace
|
|
61
|
+
? trace.error === undefined
|
|
62
|
+
? 'ready'
|
|
63
|
+
: 'failed'
|
|
64
|
+
: !tracer && report
|
|
65
|
+
? 'unavailable'
|
|
66
|
+
: 'loading'
|
|
67
|
+
const authoring = role === 'presenter' || import.meta.env.DEV
|
|
68
|
+
const wanted = !lazy || near || (!expanded && sectionStep > 0)
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!lazy || !root.current || typeof IntersectionObserver === 'undefined') {
|
|
71
|
+
setNear(true)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const observer = new IntersectionObserver(
|
|
75
|
+
(entries) => {
|
|
76
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
77
|
+
setNear(true)
|
|
78
|
+
observer.disconnect()
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{ rootMargin: '200px 0px' },
|
|
82
|
+
)
|
|
83
|
+
observer.observe(root.current)
|
|
84
|
+
return () => observer.disconnect()
|
|
85
|
+
}, [lazy])
|
|
86
|
+
const timeoutMs = timeout ?? configuredTimeout(config)
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!tracer || !wanted) return
|
|
89
|
+
let disposed = false
|
|
90
|
+
void tracer
|
|
91
|
+
.trace({
|
|
92
|
+
lang,
|
|
93
|
+
code,
|
|
94
|
+
args: stringArgs(params),
|
|
95
|
+
timeoutMs,
|
|
96
|
+
})
|
|
97
|
+
.then((result) => {
|
|
98
|
+
if (!disposed) setTrace(result)
|
|
99
|
+
})
|
|
100
|
+
.catch((error) => {
|
|
101
|
+
if (!disposed)
|
|
102
|
+
setTrace({ events: [], truncated: false, error: errorMessage(error) })
|
|
103
|
+
})
|
|
104
|
+
return () => {
|
|
105
|
+
disposed = true
|
|
106
|
+
}
|
|
107
|
+
}, [tracer, wanted, lang, code, params, timeoutMs])
|
|
108
|
+
useLayoutEffect(
|
|
109
|
+
() => markLine(source.current, frame?.line, 'pm-step-run__line'),
|
|
110
|
+
[frame?.line, children, code],
|
|
111
|
+
)
|
|
112
|
+
const go = (target: number) => setLocal(Math.max(0, Math.min(last, target)))
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
ref={root}
|
|
116
|
+
className="pm-step-run"
|
|
117
|
+
data-state={state}
|
|
118
|
+
data-steps={count}
|
|
119
|
+
data-step={frame?.step}
|
|
120
|
+
data-line={frame?.line}
|
|
121
|
+
>
|
|
122
|
+
<div ref={source} className="pm-step-run__source">
|
|
123
|
+
{children ?? (
|
|
124
|
+
<pre>
|
|
125
|
+
<code>
|
|
126
|
+
{code.split('\n').map((text, i) => (
|
|
127
|
+
<span key={i}>
|
|
128
|
+
<span className="line">{text}</span>
|
|
129
|
+
{'\n'}
|
|
130
|
+
</span>
|
|
131
|
+
))}
|
|
132
|
+
</code>
|
|
133
|
+
</pre>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
<div className="pm-step-run__side">
|
|
137
|
+
<div className="pm-step-run__status">
|
|
138
|
+
{state === 'loading' ? (
|
|
139
|
+
t.tracing
|
|
140
|
+
) : state === 'unavailable' ? (
|
|
141
|
+
t.unavailable
|
|
142
|
+
) : state === 'failed' ? (
|
|
143
|
+
t.traceFailed
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
{t.stepOf(frame?.step ?? 0, traced ?? 0)}
|
|
147
|
+
{frame?.line ? t.atLine(frame.line) : ''}
|
|
148
|
+
{trace?.truncated ? t.truncated : ''}
|
|
149
|
+
{authoring &&
|
|
150
|
+
(steps !== undefined &&
|
|
151
|
+
traced !== undefined &&
|
|
152
|
+
traced !== steps ? (
|
|
153
|
+
<div className="pm-step-run__warning">
|
|
154
|
+
{t.stepsMismatch(steps, traced)}
|
|
155
|
+
</div>
|
|
156
|
+
) : steps === undefined ? (
|
|
157
|
+
<div className="pm-step-run__warning">
|
|
158
|
+
{t.stepsMissing(traced ?? 0)}
|
|
159
|
+
</div>
|
|
160
|
+
) : null)}
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
{expanded && state === 'ready' && (
|
|
165
|
+
<div
|
|
166
|
+
className="pm-step-run__stepper"
|
|
167
|
+
role="group"
|
|
168
|
+
aria-label={t.stepper}
|
|
169
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
170
|
+
>
|
|
171
|
+
<PmIconButton
|
|
172
|
+
icon="chevron-left"
|
|
173
|
+
label={t.previousStep}
|
|
174
|
+
small
|
|
175
|
+
disabled={(frame?.step ?? 0) <= 0}
|
|
176
|
+
onClick={() => go((frame?.step ?? 0) - 1)}
|
|
177
|
+
/>
|
|
178
|
+
<input
|
|
179
|
+
type="range"
|
|
180
|
+
min="0"
|
|
181
|
+
max={last}
|
|
182
|
+
value={frame?.step ?? 0}
|
|
183
|
+
aria-label={t.stepper}
|
|
184
|
+
aria-valuetext={t.stepOf(frame?.step ?? 0, last)}
|
|
185
|
+
onChange={(e) => go(Number(e.target.value))}
|
|
186
|
+
/>
|
|
187
|
+
<PmIconButton
|
|
188
|
+
icon="chevron-right"
|
|
189
|
+
label={t.nextStep}
|
|
190
|
+
small
|
|
191
|
+
disabled={(frame?.step ?? 0) >= last}
|
|
192
|
+
onClick={() => go((frame?.step ?? 0) + 1)}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
<pre className="pm-step-run__output">
|
|
197
|
+
{trace?.error ?? frame?.output}
|
|
198
|
+
</pre>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincyaw/addon-exec",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0-rc.10",
|
|
4
|
+
"description": "Presmium plugin: run panels for code, step-by-step execution and recorded outputs; Python in the browser with Pyodide",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Lincyaw/presmium.git",
|
|
9
|
+
"directory": "packages/addon-exec"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Lincyaw/presmium/tree/main/packages/addon-exec#readme",
|
|
12
|
+
"bugs": "https://github.com/Lincyaw/presmium/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"presmium"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.19"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
"./domain": "./src/domain/index.ts",
|
|
25
|
+
"./client": "./src/client/index.ts",
|
|
26
|
+
"./presmium": "./src/node/presmium.ts",
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"LICENSES",
|
|
31
|
+
"components",
|
|
32
|
+
"src"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"diff": "^9.0.0",
|
|
36
|
+
"dompurify": "3.4.17",
|
|
37
|
+
"pyodide": "^314.0.7",
|
|
38
|
+
"sucrase": "^3.35.1",
|
|
39
|
+
"yaml": "^2.9.1"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": "^18.3.1",
|
|
43
|
+
"@lincyaw/host": "^1.0.0-rc.10",
|
|
44
|
+
"@lincyaw/kernel": "^1.0.0-rc.10"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"react": "18.3.1",
|
|
48
|
+
"@types/react": "18.3.28",
|
|
49
|
+
"@lincyaw/cli": "1.0.0-rc.10",
|
|
50
|
+
"@lincyaw/host": "1.0.0-rc.10",
|
|
51
|
+
"@lincyaw/kernel": "1.0.0-rc.10"
|
|
52
|
+
}
|
|
6
53
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.pm-run-panel__stdout,
|
|
2
|
+
.pm-run-panel__stderr {
|
|
3
|
+
margin: 0;
|
|
4
|
+
max-height: 8em;
|
|
5
|
+
overflow: auto;
|
|
6
|
+
font-family: var(--pm-font-mono);
|
|
7
|
+
font-size: 1.1em;
|
|
8
|
+
line-height: 1.6;
|
|
9
|
+
white-space: pre-wrap;
|
|
10
|
+
}
|
|
11
|
+
.pm-run-panel__stderr {
|
|
12
|
+
color: var(--pm-color-danger);
|
|
13
|
+
}
|
|
14
|
+
.pm-run-panel__image {
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
max-height: 20em;
|
|
17
|
+
object-fit: contain;
|
|
18
|
+
}
|
|
19
|
+
.pm-run-panel__html {
|
|
20
|
+
overflow: auto;
|
|
21
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Fragment, useEffect, useState } from 'react'
|
|
2
|
+
import { useMessages } from '@lincyaw/host'
|
|
3
|
+
import type { RunResult } from '@lincyaw/kernel'
|
|
4
|
+
import { MESSAGES } from './messages.ts'
|
|
5
|
+
import { displayItem } from './outputs.ts'
|
|
6
|
+
import './RunOutputs.css'
|
|
7
|
+
export default function RunOutputs({
|
|
8
|
+
result,
|
|
9
|
+
hideError = false,
|
|
10
|
+
}: {
|
|
11
|
+
result: RunResult
|
|
12
|
+
hideError?: boolean
|
|
13
|
+
}) {
|
|
14
|
+
const [mounted, setMounted] = useState(false)
|
|
15
|
+
useEffect(() => setMounted(true), [])
|
|
16
|
+
const t = useMessages(MESSAGES)
|
|
17
|
+
const outputs =
|
|
18
|
+
result.outputs && result.source !== 'recorded'
|
|
19
|
+
? result.outputs.map((output) => displayItem(output, { html: mounted }))
|
|
20
|
+
: [
|
|
21
|
+
{
|
|
22
|
+
kind: 'stream' as const,
|
|
23
|
+
name: 'stdout' as const,
|
|
24
|
+
text: result.stdout,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
kind: 'stream' as const,
|
|
28
|
+
name: 'stderr' as const,
|
|
29
|
+
text: result.stderr,
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{outputs.map((item, i) => {
|
|
35
|
+
if (item.kind === 'image')
|
|
36
|
+
return (
|
|
37
|
+
<img
|
|
38
|
+
key={i}
|
|
39
|
+
className="pm-run-panel__image"
|
|
40
|
+
src={item.src}
|
|
41
|
+
alt=""
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
if (item.kind === 'html')
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
key={i}
|
|
48
|
+
className="pm-run-panel__html"
|
|
49
|
+
dangerouslySetInnerHTML={{ __html: item.html }}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
const kind =
|
|
53
|
+
item.kind === 'stream'
|
|
54
|
+
? item.name
|
|
55
|
+
: item.kind === 'error' || item.kind === 'dropped-image'
|
|
56
|
+
? 'stderr'
|
|
57
|
+
: 'stdout'
|
|
58
|
+
const text = item.kind === 'dropped-image' ? t.droppedImage : item.text
|
|
59
|
+
return (
|
|
60
|
+
<Fragment key={i}>
|
|
61
|
+
{text && !(hideError && kind === 'stderr') && (
|
|
62
|
+
<pre className={`pm-run-panel__${kind}`}>{text}</pre>
|
|
63
|
+
)}
|
|
64
|
+
</Fragment>
|
|
65
|
+
)
|
|
66
|
+
})}
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext, useEffect } from 'react'
|
|
2
|
+
import { useStepState } from '@lincyaw/host'
|
|
3
|
+
import { STEP_MARKS } from './step-marks.ts'
|
|
4
|
+
export default function StepMark({ step }: { step: number }) {
|
|
5
|
+
const register = useContext(STEP_MARKS)
|
|
6
|
+
const state = useStepState({ from: step })
|
|
7
|
+
useEffect(
|
|
8
|
+
() => register?.(step, state),
|
|
9
|
+
[register, step, state.reached, state.active],
|
|
10
|
+
)
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ExecutorRouter, RunRequest, RunResult } from '@lincyaw/kernel'
|
|
2
|
+
import { mimeText } from '../domain/index.ts'
|
|
3
|
+
|
|
4
|
+
const LIMIT = 8
|
|
5
|
+
const TEXT_LIMIT = 2000
|
|
6
|
+
|
|
7
|
+
interface Feedback {
|
|
8
|
+
id: number
|
|
9
|
+
at: number
|
|
10
|
+
type: 'run' | 'reset-session'
|
|
11
|
+
state: 'running' | 'succeeded' | 'failed' | 'timed-out' | 'reset'
|
|
12
|
+
section?: string
|
|
13
|
+
block?: string
|
|
14
|
+
lang?: string
|
|
15
|
+
session?: string
|
|
16
|
+
executor?: string
|
|
17
|
+
exitCode?: number | null
|
|
18
|
+
source?: 'live' | 'recorded'
|
|
19
|
+
durationMs?: number
|
|
20
|
+
stdout?: string
|
|
21
|
+
stderr?: string
|
|
22
|
+
value?: string
|
|
23
|
+
displayTypes?: string[]
|
|
24
|
+
truncated?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resultInfo(result: RunResult): Partial<Feedback> {
|
|
28
|
+
const values = result.outputs?.flatMap(output => output.output_type === 'execute_result'
|
|
29
|
+
? [output.data['text/plain'] ?? output.data['text/markdown'] ?? output.data['application/json']]
|
|
30
|
+
: []) ?? []
|
|
31
|
+
const value = values.map(value => mimeText(value)).join('\n')
|
|
32
|
+
const displayTypes = [...new Set(result.outputs?.flatMap(output => output.output_type === 'execute_result' || output.output_type === 'display_data' ? Object.keys(output.data) : []) ?? [])]
|
|
33
|
+
return {
|
|
34
|
+
state: result.timedOut ? 'timed-out' : result.ok ? 'succeeded' : 'failed',
|
|
35
|
+
executor: result.executorId,
|
|
36
|
+
exitCode: result.exitCode,
|
|
37
|
+
source: result.source,
|
|
38
|
+
durationMs: result.durationMs,
|
|
39
|
+
stdout: result.stdout.slice(0, TEXT_LIMIT),
|
|
40
|
+
stderr: result.stderr.slice(0, TEXT_LIMIT),
|
|
41
|
+
...value ? { value: value.slice(0, TEXT_LIMIT) } : {},
|
|
42
|
+
...displayTypes.length ? { displayTypes } : {},
|
|
43
|
+
...[result.stdout, result.stderr, value].some(text => text.length > TEXT_LIMIT) ? { truncated: true } : {},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Status follows the shared execution boundary, so every browser executor reports the same way. */
|
|
48
|
+
export function withRuntimeFeedback(router: ExecutorRouter): { executor: ExecutorRouter, read(): { recent: Feedback[] } } {
|
|
49
|
+
let sequence = 0
|
|
50
|
+
let recent: Feedback[] = []
|
|
51
|
+
const add = (entry: Feedback) => { recent = [...recent, entry].slice(-LIMIT) }
|
|
52
|
+
return {
|
|
53
|
+
read: () => ({ recent }),
|
|
54
|
+
executor: {
|
|
55
|
+
...router,
|
|
56
|
+
async run(request: RunRequest, signal?: AbortSignal) {
|
|
57
|
+
const id = ++sequence
|
|
58
|
+
add({ id, at: Date.now(), type: 'run', state: 'running', lang: request.lang, ...request.place ? { section: request.place.section, block: request.place.block } : {}, ...request.session ? { session: request.session } : {} })
|
|
59
|
+
const result = await router.run(request, signal)
|
|
60
|
+
recent = recent.map(entry => entry.id === id ? { ...entry, ...resultInfo(result) } : entry)
|
|
61
|
+
return result
|
|
62
|
+
},
|
|
63
|
+
async endSession(session) {
|
|
64
|
+
await router.endSession?.(session)
|
|
65
|
+
add({ id: ++sequence, at: Date.now(), type: 'reset-session', state: 'reset', session })
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const NAMES: Record<string, string> = {
|
|
2
|
+
'python': 'Python',
|
|
3
|
+
'py': 'Python',
|
|
4
|
+
'python3': 'Python',
|
|
5
|
+
'javascript': 'JavaScript',
|
|
6
|
+
'js': 'JavaScript',
|
|
7
|
+
'typescript': 'TypeScript',
|
|
8
|
+
'ts': 'TypeScript',
|
|
9
|
+
'wgsl': 'WGSL',
|
|
10
|
+
'c': 'C',
|
|
11
|
+
'cpp': 'C++',
|
|
12
|
+
'c++': 'C++',
|
|
13
|
+
'ruby': 'Ruby',
|
|
14
|
+
'rb': 'Ruby',
|
|
15
|
+
'sql': 'SQL',
|
|
16
|
+
'r': 'R',
|
|
17
|
+
'julia': 'Julia',
|
|
18
|
+
'bash': 'Bash',
|
|
19
|
+
'sh': 'Shell',
|
|
20
|
+
'shell': 'Shell',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function languageName(lang: string): string {
|
|
24
|
+
return NAMES[lang.toLowerCase()] ?? lang
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function seconds(ms: number): string {
|
|
28
|
+
const s = Math.max(0, ms) / 1000
|
|
29
|
+
return s < 10 ? s.toFixed(2) : s < 100 ? s.toFixed(1) : String(Math.round(s))
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { execPlugin } from './plugin.ts'
|
|
2
|
+
export { messages, MESSAGES } from './messages.ts'
|
|
3
|
+
export type { ExecMessages } from './messages.ts'
|
|
4
|
+
export { languageName, seconds } from './format.ts'
|
|
5
|
+
export { runsInOf } from './runs-in.ts'
|
|
6
|
+
export type { RunsIn } from './runs-in.ts'
|
|
7
|
+
export { createPyodideExecutor, PYODIDE_EXECUTOR_ID } from './pyodide/executor.ts'
|
|
8
|
+
export type { PyodideExecutor, PyodideExecutorOptions } from './pyodide/executor.ts'
|
|
9
|
+
export { BROWSER_SCRIPT_EXECUTOR_ID, createScriptExecutor } from './script/executor.ts'
|
|
10
|
+
export type { ScriptExecutor } from './script/executor.ts'
|
|
11
|
+
export { whenService } from './when-service.ts'
|
|
12
|
+
export { markLine, STEP_MARKS } from './step-marks.ts'
|
|
13
|
+
export type { RegisterStepMark } from './step-marks.ts'
|
|
14
|
+
export { useSlideSpaceBelow } from './slide-fit.ts'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { currentLocale } from '@lincyaw/host'
|
|
2
|
+
import type { Catalog } from '@lincyaw/host'
|
|
3
|
+
|
|
4
|
+
const zhCN = {
|
|
5
|
+
run: '运行',
|
|
6
|
+
rerun: '重新运行',
|
|
7
|
+
running: '正在运行…',
|
|
8
|
+
unavailable: '此处无法运行代码',
|
|
9
|
+
timedOut: '运行超时',
|
|
10
|
+
failed: '运行失败',
|
|
11
|
+
recordedBadge: (reason: string) => `预录输出 · ${reason}`,
|
|
12
|
+
recordedLine: (reason: string) => `预录输出(${reason})`,
|
|
13
|
+
recordedOutput: '预录输出',
|
|
14
|
+
runHint: (icon: string) => `按 ${icon} 运行这段代码`,
|
|
15
|
+
runsIn: {
|
|
16
|
+
browser: '浏览器中运行',
|
|
17
|
+
machine: '本机运行',
|
|
18
|
+
remote: (name: string) => `在远端环境 ${name} 中运行`,
|
|
19
|
+
environment: (name: string) => `在环境 ${name} 中运行`,
|
|
20
|
+
},
|
|
21
|
+
output: '输出',
|
|
22
|
+
outputTook: (seconds: string) => `输出 · ${seconds} 秒`,
|
|
23
|
+
droppedImage: '图片输出的数据不是 base64,未显示',
|
|
24
|
+
gpuUnavailable: (detail: string) => (/no gpu adapter/i.test(detail) ? '没有可用的 GPU 适配器' : /no webgpu/i.test(detail) ? '浏览器没有 WebGPU' : `GPU 不可用:${detail}`),
|
|
25
|
+
recorded: '预录',
|
|
26
|
+
succeeded: '成功',
|
|
27
|
+
failedShort: '失败',
|
|
28
|
+
compare: (before: number, after: number) => `对比 #${before} → #${after}`,
|
|
29
|
+
argChange: (name: string, before: string, after: string) => `${name}:${before} → ${after}`,
|
|
30
|
+
tracing: '正在追踪执行…',
|
|
31
|
+
traceFailed: '追踪失败',
|
|
32
|
+
stepOf: (step: number, total: number) => `第 ${step} / ${total} 步`,
|
|
33
|
+
atLine: (line: number) => ` · 第 ${line} 行`,
|
|
34
|
+
truncated: '(已截断)',
|
|
35
|
+
stepsMismatch: (declared: number, traced: number) => `steps 声明为 ${declared},实际执行 ${traced} 行`,
|
|
36
|
+
stepsMissing: (traced: number) => `未声明 steps(应为 ${traced}),直接打开某一步时无法恢复`,
|
|
37
|
+
cancelled: '已取消',
|
|
38
|
+
runTimeout: (ms: number) => `执行超过 ${ms} ms,已停止`,
|
|
39
|
+
runtimeCrashed: (runtime: string, error: string) => `无法运行 ${runtime} 代码:${error}`,
|
|
40
|
+
loadTimeout: (runtime: string, ms: number) => `${runtime} 运行时 ${ms} ms 内未加载完成,已停止`,
|
|
41
|
+
noExecutor: (lang: string) => `此处无法运行 ${lang} 代码`,
|
|
42
|
+
executorUnavailable: (id: string, lang: string) => `运行环境 ${id} 不可用(未设置,或不能运行 ${lang})`,
|
|
43
|
+
resetSession: '重置会话',
|
|
44
|
+
executorCrashed: (_id: string, error: string) => `无法运行代码:${error}`,
|
|
45
|
+
stepper: '逐行执行的步骤',
|
|
46
|
+
previousStep: '上一步',
|
|
47
|
+
nextStep: '下一步',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ExecMessages = typeof zhCN
|
|
51
|
+
|
|
52
|
+
const en: ExecMessages = {
|
|
53
|
+
run: 'Run',
|
|
54
|
+
rerun: 'Run Again',
|
|
55
|
+
running: 'Running…',
|
|
56
|
+
unavailable: 'Code can’t run here',
|
|
57
|
+
timedOut: 'timed out',
|
|
58
|
+
failed: 'run failed',
|
|
59
|
+
recordedBadge: reason => `Recorded output · ${reason}`,
|
|
60
|
+
recordedLine: reason => `Recorded output (${reason})`,
|
|
61
|
+
recordedOutput: 'Recorded output',
|
|
62
|
+
runHint: icon => `Press ${icon} to run this code`,
|
|
63
|
+
runsIn: {
|
|
64
|
+
browser: 'runs in the browser',
|
|
65
|
+
machine: 'runs on this machine',
|
|
66
|
+
remote: name => `runs in the remote environment ${name}`,
|
|
67
|
+
environment: name => `runs in the environment ${name}`,
|
|
68
|
+
},
|
|
69
|
+
output: 'Output',
|
|
70
|
+
outputTook: seconds => `Output · ${seconds} s`,
|
|
71
|
+
droppedImage: 'The picture output does not hold base64 data and is not shown',
|
|
72
|
+
gpuUnavailable: detail => (/no gpu adapter/i.test(detail) ? 'No GPU adapter is available' : /no webgpu/i.test(detail) ? 'This browser has no WebGPU' : `The GPU is unavailable: ${detail}`),
|
|
73
|
+
recorded: 'recorded',
|
|
74
|
+
succeeded: 'ok',
|
|
75
|
+
failedShort: 'failed',
|
|
76
|
+
compare: (before, after) => `Compare #${before} → #${after}`,
|
|
77
|
+
argChange: (name, before, after) => `${name}: ${before} → ${after}`,
|
|
78
|
+
tracing: 'Tracing the run…',
|
|
79
|
+
traceFailed: 'Tracing failed',
|
|
80
|
+
stepOf: (step, total) => `Step ${step} / ${total}`,
|
|
81
|
+
atLine: line => ` · line ${line}`,
|
|
82
|
+
truncated: ' (truncated)',
|
|
83
|
+
stepsMismatch: (declared, traced) => `steps is declared as ${declared}, but ${traced} lines ran`,
|
|
84
|
+
stepsMissing: traced => `steps is not declared (it should be ${traced}); opening a step directly cannot restore it`,
|
|
85
|
+
cancelled: 'Canceled',
|
|
86
|
+
runTimeout: ms => `Stopped after ${ms} ms`,
|
|
87
|
+
runtimeCrashed: (runtime, error) => `Couldn’t run the ${runtime} code: ${error}`,
|
|
88
|
+
loadTimeout: (runtime, ms) => `The ${runtime} runtime did not load within ${ms} ms; stopped`,
|
|
89
|
+
noExecutor: lang => `${lang} code can’t run here`,
|
|
90
|
+
executorUnavailable: (id, lang) => `The run environment ${id} is unavailable (not set up, or it can’t run ${lang})`,
|
|
91
|
+
resetSession: 'Reset Session',
|
|
92
|
+
executorCrashed: (_id, error) => `Couldn’t run the code: ${error}`,
|
|
93
|
+
stepper: 'Steps of the run',
|
|
94
|
+
previousStep: 'Previous Step',
|
|
95
|
+
nextStep: 'Next Step',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const MESSAGES: Catalog<ExecMessages> = { 'zh-CN': zhCN, en }
|
|
99
|
+
|
|
100
|
+
export function messages(): ExecMessages {
|
|
101
|
+
return MESSAGES[currentLocale()]
|
|
102
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RunOutput } from '@lincyaw/kernel'
|
|
2
|
+
import DOMPurify from 'dompurify'
|
|
3
|
+
import { mimeText } from '../domain/index.ts'
|
|
4
|
+
|
|
5
|
+
export type DisplayItem =
|
|
6
|
+
| { kind: 'stream', name: 'stdout' | 'stderr', text: string }
|
|
7
|
+
| { kind: 'error', text: string }
|
|
8
|
+
| { kind: 'image', src: string }
|
|
9
|
+
| { kind: 'dropped-image' }
|
|
10
|
+
| { kind: 'html', html: string }
|
|
11
|
+
| { kind: 'text', text: string }
|
|
12
|
+
|
|
13
|
+
const IMAGES = ['image/png', 'image/jpeg', 'image/gif']
|
|
14
|
+
const BASE64 = /^[A-Za-z0-9+/=]*$/
|
|
15
|
+
|
|
16
|
+
/** What to show of one output: an image, sanitized HTML (only where `html` allows it) or text, in this order of preference. */
|
|
17
|
+
export function displayItem(output: RunOutput, options: { html: boolean }): DisplayItem {
|
|
18
|
+
if (output.output_type === 'stream') return { kind: 'stream', name: output.name, text: mimeText(output.text) }
|
|
19
|
+
if (output.output_type === 'error') return { kind: 'error', text: output.traceback.join('\n') || `${output.ename}: ${output.evalue}` }
|
|
20
|
+
const { data } = output
|
|
21
|
+
const image = IMAGES.find(type => data[type] !== undefined)
|
|
22
|
+
if (image) {
|
|
23
|
+
const base64 = mimeText(data[image], 2).replace(/\s/g, '')
|
|
24
|
+
return BASE64.test(base64) ? { kind: 'image', src: `data:${image};base64,${base64}` } : { kind: 'dropped-image' }
|
|
25
|
+
}
|
|
26
|
+
if (data['image/svg+xml'] !== undefined) return { kind: 'image', src: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(mimeText(data['image/svg+xml'], 2))}` }
|
|
27
|
+
if (data['text/html'] !== undefined && options.html) return { kind: 'html', html: DOMPurify.sanitize(mimeText(data['text/html'], 2)) }
|
|
28
|
+
return { kind: 'text', text: mimeText(data['text/markdown'] ?? data['text/plain'] ?? data['application/json'] ?? data['text/latex'], 2) }
|
|
29
|
+
}
|