feat(editor): NodeView 适配 onRunCode 协议(extractLang + overridesJson 提取)

runCode 改传纯语言名(extractLang) + overrides JSON 字符串(extractOverridesJson),
对齐 Task 4 Rust 桥接 RunCodeOptsJs(language/source/overrides_json):
- highlight.ts: 新增 extractOverridesJson,对齐后端 parse_fence_info 取以 { 开头的 token
- code-block-view.ts: RunCodeOpts 加 overridesJson 字段,runCode 改传 extractLang/extractOverridesJson 提取结果
- index.ts: EditorOptions.onRunCode 类型同步加 overridesJson
- 测试: extractOverridesJson 6 例 + code-block-view 点击断言更新(纯语言名 + overridesJson)
This commit is contained in:
xfy 2026-07-06 18:17:33 +08:00
parent 0c1e8b7640
commit 763edc300d
5 changed files with 52 additions and 8 deletions

View File

@ -97,13 +97,17 @@ describe('CodeBlockNodeView', () => {
it('点击运行按钮调用 onRunCode(storage 回调)', () => { it('点击运行按钮调用 onRunCode(storage 回调)', () => {
const onRunCode = vi.fn(() => Promise.resolve('结果')); const onRunCode = vi.fn(() => Promise.resolve('结果'));
const view = new CodeBlockNodeView({ const view = new CodeBlockNodeView({
node: mockNode('python runnable', 'print(1)'), node: mockNode('python runnable {"timeout_secs":10}', 'print(1)'),
editor: mockEditor(onRunCode), editor: mockEditor(onRunCode),
} as any); } as any);
view.dom.querySelector<HTMLButtonElement>('.tiptap-codeblock-run')!.click(); view.dom.querySelector<HTMLButtonElement>('.tiptap-codeblock-run')!.click();
expect(onRunCode).toHaveBeenCalledTimes(1); expect(onRunCode).toHaveBeenCalledTimes(1);
expect(onRunCode).toHaveBeenCalledWith( expect(onRunCode).toHaveBeenCalledWith(
expect.objectContaining({ language: 'python runnable', source: 'print(1)' }), expect.objectContaining({
language: 'python',
source: 'print(1)',
overridesJson: '{"timeout_secs":10}',
}),
); );
}); });

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { extractLang, lowlight } from '../highlight'; import { extractLang, extractOverridesJson, lowlight } from '../highlight';
/** /**
* extractLang fence info string token * 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 * lowlight wrapper highlight extractLang
* runnable info string * runnable info string

View File

@ -1,16 +1,18 @@
import type { Editor } from '@tiptap/core'; import type { Editor } from '@tiptap/core';
import type { Node as PMNode } from '@tiptap/pm/model'; import type { Node as PMNode } from '@tiptap/pm/model';
import { extractLang } from './highlight'; import { extractLang, extractOverridesJson } from './highlight';
/** editor.storage 的 key宿主index.ts在此注入 onRunCode 回调。 */ /** editor.storage 的 key宿主index.ts在此注入 onRunCode 回调。 */
export const ON_RUN_CODE_STORAGE_KEY = '__onRunCode'; export const ON_RUN_CODE_STORAGE_KEY = '__onRunCode';
/** 运行请求参数(传给 onRunCode 回调)。 */ /** 运行请求参数(传给 onRunCode 回调)。 */
export interface RunCodeOpts { export interface RunCodeOpts {
/** 完整 info string如 `python runnable {"timeout_secs":10}`)。 */ /** 纯语言名extractLang 提取,如 "python")。 */
language: string; language: string;
/** 代码块文本内容。 */ /** 代码块文本内容。 */
source: string; source: string;
/** overrides 的 JSON 字符串extractOverridesJson 提取,空串表示无 overrides。 */
overridesJson: string;
} }
/** /**
@ -153,9 +155,11 @@ export class CodeBlockNodeView {
this.ensureResultArea('运行中…'); this.ensureResultArea('运行中…');
try { try {
const info = (this.node.attrs.language as string) ?? '';
const result = await onRunCode({ const result = await onRunCode({
language: (this.node.attrs.language as string) ?? '', language: extractLang(info),
source: this.node.textContent, source: this.node.textContent,
overridesJson: extractOverridesJson(info),
}); });
this.renderResult(result); this.renderResult(result);
} catch (e) { } catch (e) {

View File

@ -14,6 +14,21 @@ export function extractLang(info: string): string {
return first ? first.toLowerCase() : ''; return first ? first.toLowerCase() : '';
} }
/**
* fence info string overrides JSON
*
* parse_fence_infosrc/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 * lowlight highlight info string
* *

View File

@ -40,8 +40,12 @@ class EditorOptions {
// 上传状态事件error/success/removed + counts替代 window.__tiptap_uploads 轮询 // 上传状态事件error/success/removed + counts替代 window.__tiptap_uploads 轮询
onUploadEvent?: (event: UploadEvent) => void; onUploadEvent?: (event: UploadEvent) => void;
// 编辑器内运行代码回调Rust 注入NodeView 点击「运行」时经 editor.storage 转发。 // 编辑器内运行代码回调Rust 注入NodeView 点击「运行」时经 editor.storage 转发。
// opts: { language, source }返回格式化结果字符串Rust 拼好 stdout/stderr/状态)。 // opts: { language(纯语言名), source, overridesJson }返回格式化结果字符串Rust 拼好 stdout/stderr/状态)。
onRunCode?: (opts: { language: string; source: string }) => Promise<string>; onRunCode?: (opts: {
language: string;
source: string;
overridesJson: string;
}) => Promise<string>;
} }
// wasm-bindgen 生成的 glue 用裸标识符 `new EditorOptions()` 从全局解析, // wasm-bindgen 生成的 glue 用裸标识符 `new EditorOptions()` 从全局解析,
// IIFE 的 name 只能挂一个全局TiptapEditor这里手动把 EditorOptions 也挂到 window 上。 // IIFE 的 name 只能挂一个全局TiptapEditor这里手动把 EditorOptions 也挂到 window 上。