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:
parent
0c1e8b7640
commit
763edc300d
@ -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}',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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 也能按正确语言高亮。
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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_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。
|
* lowlight 实例,包装 highlight 方法以处理完整 info string。
|
||||||
*
|
*
|
||||||
|
|||||||
@ -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 上。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user