@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.
Files changed (52) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSES +696 -0
  3. package/README.md +33 -2
  4. package/components/PmRunPanel.css +167 -0
  5. package/components/PmRunPanel.tsx +389 -0
  6. package/components/PmStepRun.css +61 -0
  7. package/components/PmStepRun.tsx +202 -0
  8. package/package.json +50 -3
  9. package/src/client/RunOutputs.css +21 -0
  10. package/src/client/RunOutputs.tsx +69 -0
  11. package/src/client/StepMark.css +3 -0
  12. package/src/client/StepMark.tsx +12 -0
  13. package/src/client/env.d.ts +13 -0
  14. package/src/client/feedback.ts +69 -0
  15. package/src/client/format.ts +30 -0
  16. package/src/client/index.ts +14 -0
  17. package/src/client/messages.ts +102 -0
  18. package/src/client/outputs.ts +29 -0
  19. package/src/client/plugin.ts +55 -0
  20. package/src/client/pyodide/executor.ts +94 -0
  21. package/src/client/pyodide/presmium_gpu.py +297 -0
  22. package/src/client/pyodide/protocol.ts +27 -0
  23. package/src/client/pyodide/pyodide.worker.ts +68 -0
  24. package/src/client/pyodide/run_await.py +57 -0
  25. package/src/client/runs-in.ts +27 -0
  26. package/src/client/script/executor.ts +62 -0
  27. package/src/client/script/script.worker.ts +9 -0
  28. package/src/client/slide-fit.ts +61 -0
  29. package/src/client/step-marks.ts +7 -0
  30. package/src/client/when-service.ts +14 -0
  31. package/src/domain/browser-python.ts +33 -0
  32. package/src/domain/config.ts +41 -0
  33. package/src/domain/index.ts +11 -0
  34. package/src/domain/outputs.ts +18 -0
  35. package/src/domain/router.ts +50 -0
  36. package/src/domain/runs.ts +72 -0
  37. package/src/domain/static-output.ts +20 -0
  38. package/src/domain/trace.ts +32 -0
  39. package/src/node/browser-imports.ts +34 -0
  40. package/src/node/child-process.ts +109 -0
  41. package/src/node/code-component.ts +48 -0
  42. package/src/node/notebook-cells.ts +46 -0
  43. package/src/node/presmium.ts +27 -0
  44. package/src/node/pyodide.ts +22 -0
  45. package/src/node/python.ts +40 -0
  46. package/src/node/run-panel.ts +71 -0
  47. package/src/node/script-main.mjs +11 -0
  48. package/src/node/script.ts +26 -0
  49. package/src/node/step-run.ts +71 -0
  50. package/src/node/timeouts.ts +15 -0
  51. package/src/shared/script-runner.d.mts +16 -0
  52. 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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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,3 @@
1
+ .pm-step-mark {
2
+ display: none;
3
+ }
@@ -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,13 @@
1
+ interface ImportMetaEnv {
2
+ BASE_URL: string
3
+ DEV: boolean
4
+ }
5
+
6
+ interface ImportMeta {
7
+ readonly env: ImportMetaEnv
8
+ }
9
+
10
+ declare module '*.py?raw' {
11
+ const source: string
12
+ export default source
13
+ }
@@ -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
+ }