diff --git a/libs/tiptap-editor/src/__tests__/code-block-view.test.ts b/libs/tiptap-editor/src/__tests__/code-block-view.test.ts index f0e8331..846dcea 100644 --- a/libs/tiptap-editor/src/__tests__/code-block-view.test.ts +++ b/libs/tiptap-editor/src/__tests__/code-block-view.test.ts @@ -97,13 +97,17 @@ describe('CodeBlockNodeView', () => { it('点击运行按钮调用 onRunCode(storage 回调)', () => { const onRunCode = vi.fn(() => Promise.resolve('结果')); const view = new CodeBlockNodeView({ - node: mockNode('python runnable', 'print(1)'), + node: mockNode('python runnable {"timeout_secs":10}', 'print(1)'), editor: mockEditor(onRunCode), } as any); view.dom.querySelector('.tiptap-codeblock-run')!.click(); expect(onRunCode).toHaveBeenCalledTimes(1); expect(onRunCode).toHaveBeenCalledWith( - expect.objectContaining({ language: 'python runnable', source: 'print(1)' }), + expect.objectContaining({ + language: 'python', + source: 'print(1)', + overridesJson: '{"timeout_secs":10}', + }), ); }); diff --git a/libs/tiptap-editor/src/__tests__/highlight.test.ts b/libs/tiptap-editor/src/__tests__/highlight.test.ts index 397c380..67d2f91 100644 --- a/libs/tiptap-editor/src/__tests__/highlight.test.ts +++ b/libs/tiptap-editor/src/__tests__/highlight.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { extractLang, lowlight } from '../highlight'; +import { extractLang, extractOverridesJson, lowlight } from '../highlight'; /** * extractLang 测试:从完整 fence info string 提取语言名(首个 token)。 @@ -21,6 +21,23 @@ describe('extractLang', () => { }); }); +describe('extractOverridesJson', () => { + it.each([ + ['python runnable {"timeout_secs":10}', '{"timeout_secs":10}', 'runnable + overrides'], + [ + 'python runnable {"timeout_secs":10,"memory_mb":256}', + '{"timeout_secs":10,"memory_mb":256}', + '多字段 overrides', + ], + ['python runnable', '', '无 overrides'], + ['python', '', '纯语言名'], + ['', '', '空字符串'], + ['python runnable {"allow_network":true}', '{"allow_network":true}', 'allow_network'], + ])('%s → %s (%s)', (input, expected) => { + expect(extractOverridesJson(input)).toBe(expected); + }); +}); + /** * lowlight wrapper 测试:highlight 经过 extractLang 处理, * runnable info string 也能按正确语言高亮。 diff --git a/libs/tiptap-editor/src/code-block-view.ts b/libs/tiptap-editor/src/code-block-view.ts index 117e224..4b21a7d 100644 --- a/libs/tiptap-editor/src/code-block-view.ts +++ b/libs/tiptap-editor/src/code-block-view.ts @@ -1,16 +1,18 @@ import type { Editor } from '@tiptap/core'; import type { Node as PMNode } from '@tiptap/pm/model'; -import { extractLang } from './highlight'; +import { extractLang, extractOverridesJson } from './highlight'; /** editor.storage 的 key,宿主(index.ts)在此注入 onRunCode 回调。 */ export const ON_RUN_CODE_STORAGE_KEY = '__onRunCode'; /** 运行请求参数(传给 onRunCode 回调)。 */ export interface RunCodeOpts { - /** 完整 info string(如 `python runnable {"timeout_secs":10}`)。 */ + /** 纯语言名(extractLang 提取,如 "python")。 */ language: string; /** 代码块文本内容。 */ source: string; + /** overrides 的 JSON 字符串(extractOverridesJson 提取,空串表示无 overrides)。 */ + overridesJson: string; } /** @@ -153,9 +155,11 @@ export class CodeBlockNodeView { this.ensureResultArea('运行中…'); try { + const info = (this.node.attrs.language as string) ?? ''; const result = await onRunCode({ - language: (this.node.attrs.language as string) ?? '', + language: extractLang(info), source: this.node.textContent, + overridesJson: extractOverridesJson(info), }); this.renderResult(result); } catch (e) { diff --git a/libs/tiptap-editor/src/highlight.ts b/libs/tiptap-editor/src/highlight.ts index e0b0d54..356c5b0 100644 --- a/libs/tiptap-editor/src/highlight.ts +++ b/libs/tiptap-editor/src/highlight.ts @@ -14,6 +14,21 @@ export function extractLang(info: string): string { return first ? first.toLowerCase() : ''; } +/** + * 从完整 fence info string 提取 overrides JSON 字符串。 + * + * 与后端 parse_fence_info(src/api/code_runner/languages.rs:93-97)对齐: + * info string 形如 `python runnable {"timeout_secs":10}`,提取以 `{` 开头的 token。 + * 无 overrides 时返回空串(Rust 侧空串视为 None)。 + */ +export function extractOverridesJson(info: string): string { + const token = info + .trim() + .split(/\s+/) + .find((t) => t.startsWith('{')); + return token ?? ''; +} + /** * lowlight 实例,包装 highlight 方法以处理完整 info string。 * diff --git a/libs/tiptap-editor/src/index.ts b/libs/tiptap-editor/src/index.ts index 87feabe..8d87df6 100644 --- a/libs/tiptap-editor/src/index.ts +++ b/libs/tiptap-editor/src/index.ts @@ -40,8 +40,12 @@ class EditorOptions { // 上传状态事件(error/success/removed + counts),替代 window.__tiptap_uploads 轮询 onUploadEvent?: (event: UploadEvent) => void; // 编辑器内运行代码回调:Rust 注入,NodeView 点击「运行」时经 editor.storage 转发。 - // opts: { language, source };返回格式化结果字符串(Rust 拼好 stdout/stderr/状态)。 - onRunCode?: (opts: { language: string; source: string }) => Promise; + // opts: { language(纯语言名), source, overridesJson };返回格式化结果字符串(Rust 拼好 stdout/stderr/状态)。 + onRunCode?: (opts: { + language: string; + source: string; + overridesJson: string; + }) => Promise; } // wasm-bindgen 生成的 glue 用裸标识符 `new EditorOptions()` 从全局解析, // IIFE 的 name 只能挂一个全局(TiptapEditor),这里手动把 EditorOptions 也挂到 window 上。