import type { Editor } from '@tiptap/core'; import type { Node as PMNode } from '@tiptap/pm/model'; import { extractLang, extractOverridesJson } from './highlight'; /** editor.storage 的 key,宿主(index.ts)在此注入 onRunCode 回调。 */ export const ON_RUN_CODE_STORAGE_KEY = '__onRunCode'; /** 运行请求参数(传给 onRunCode 回调)。 */ export interface RunCodeOpts { /** 纯语言名(extractLang 提取,如 "python")。 */ language: string; /** 代码块文本内容。 */ source: string; /** overrides 的 JSON 字符串(extractOverridesJson 提取,空串表示无 overrides)。 */ overridesJson: string; } /** * 判断节点是否为 runnable 代码块(language 属性含 'runnable' 标记)。 */ function isRunnable(node: PMNode): boolean { const lang = (node.attrs.language as string | null) ?? ''; return lang.includes('runnable'); } /** * CodeBlock 自定义 NodeView。 * * 在标准 `
` 外包一层容器,顶部加工具栏(语言标签 + 运行按钮),
 * 底部加运行结果区。contentDOM 仍指向 ``,保证 CodeBlockLowlight 的
 * decoration(语法高亮)正常生效。
 *
 * 运行按钮仅 runnable 块显示;点击调 editor.storage.__onRunCode 回调
 * (由 index.ts 注入,转发到 Rust server function)。
 */
export class CodeBlockNodeView {
  private node: PMNode;
  private editor: Editor;

  private container: HTMLDivElement;
  private toolbar: HTMLDivElement;
  private langBadge: HTMLSpanElement;
  private runBtn: HTMLButtonElement | null = null;
  private pre: HTMLPreElement;
  private code: HTMLElement;
  private resultArea: HTMLDivElement | null = null;

  constructor(opts: { node: PMNode; editor: Editor }) {
    this.node = opts.node;
    this.editor = opts.editor;

    this.container = document.createElement('div');
    this.container.classList.add('tiptap-codeblock');

    // 工具栏
    this.toolbar = document.createElement('div');
    this.toolbar.classList.add('tiptap-codeblock-toolbar');
    this.toolbar.setAttribute('contenteditable', 'false');

    this.langBadge = document.createElement('span');
    this.langBadge.classList.add('tiptap-codeblock-lang');
    this.langBadge.textContent = extractLang((this.node.attrs.language as string) ?? '');
    this.toolbar.appendChild(this.langBadge);

    // 运行按钮(仅 runnable 块)
    if (isRunnable(this.node)) {
      this.runBtn = this.createRunButton();
      this.toolbar.appendChild(this.runBtn);
    }

    this.container.appendChild(this.toolbar);

    // pre > code(contentDOM,decoration 在此生效)
    this.pre = document.createElement('pre');
    this.code = document.createElement('code');
    const lang = (this.node.attrs.language as string) ?? '';
    if (lang) {
      this.code.classList.add(`language-${lang}`);
    }
    this.pre.appendChild(this.code);
    this.container.appendChild(this.pre);
  }

  get dom(): HTMLElement {
    return this.container;
  }

  get contentDOM(): HTMLElement | null {
    // contentDOM 必须是 ,ProseMirror 在此挂 decoration + 处理文本编辑。
    return this.code;
  }

  /** ProseMirror 调用:节点属性变化时刷新工具栏。返回 false 拒绝非同类节点。 */
  update(node: PMNode): boolean {
    // 按 node.type 引用比较:真实 ProseMirror 每个 schema 的 NodeType 是单例,
    // 同类节点引用必等(对齐 upload-image.ts 既有范式)。
    if (node.type !== this.node.type) return false;
    const oldLang = (this.node.attrs.language as string) ?? '';
    const newLang = (node.attrs.language as string) ?? '';
    this.node = node;
    if (oldLang !== newLang) {
      this.langBadge.textContent = extractLang(newLang);
      // runnable 状态变化时重建按钮(简化:不细粒度增删,整体重建工具栏按钮区)
      this.refreshRunButton();
    }
    return true;
  }

  /** 语言变化后,按 runnable 状态增删运行按钮。 */
  private refreshRunButton(): void {
    const shouldHave = isRunnable(this.node);
    const has = this.runBtn !== null;
    if (shouldHave && !has) {
      this.runBtn = this.createRunButton();
      this.toolbar.appendChild(this.runBtn);
    } else if (!shouldHave && has) {
      this.runBtn?.remove();
      this.runBtn = null;
    }
  }

  /** 工具栏/结果区交互不触发 ProseMirror 编辑事务。 */
  ignoreMutation(): boolean {
    return true;
  }

  /** 工具栏按钮点击不被编辑器拦截。 */
  stopEvent(): boolean {
    return false;
  }

  /** 创建运行按钮(构造与 update 增删共用)。 */
  private createRunButton(): HTMLButtonElement {
    const btn = document.createElement('button');
    btn.classList.add('tiptap-codeblock-run');
    btn.type = 'button';
    btn.textContent = '▶ 运行';
    btn.setAttribute('contenteditable', 'false');
    btn.addEventListener('click', () => this.runCode());
    return btn;
  }

  /** 点击运行:调 editor.storage.__onRunCode,结果填入结果区。 */
  private async runCode(): Promise {
    if (!this.runBtn) return;
    const storage = this.editor.storage as unknown as Record;
    const onRunCode = storage[ON_RUN_CODE_STORAGE_KEY] as
      | ((opts: RunCodeOpts) => Promise)
      | undefined;
    if (!onRunCode) return; // 优雅降级:无回调时不操作

    // 防重复点击
    this.runBtn.disabled = true;
    this.runBtn.textContent = '运行中…';
    this.ensureResultArea('运行中…');

    try {
      const info = (this.node.attrs.language as string) ?? '';
      const result = await onRunCode({
        language: extractLang(info),
        source: this.node.textContent,
        overridesJson: extractOverridesJson(info),
      });
      this.renderResult(result);
    } catch (e) {
      this.renderResult(`运行失败:${e instanceof Error ? e.message : String(e)}`);
    } finally {
      this.runBtn.disabled = false;
      this.runBtn.textContent = '▶ 运行';
    }
  }

  /** 确保 resultArea 存在,设置初始内容。 */
  private ensureResultArea(initial: string): void {
    if (!this.resultArea) {
      this.resultArea = document.createElement('div');
      this.resultArea.classList.add('tiptap-codeblock-result');
      this.resultArea.setAttribute('contenteditable', 'false');
      this.container.appendChild(this.resultArea);
    }
    this.resultArea.textContent = initial;
  }

  /** 渲染运行结果字符串到结果区。 */
  private renderResult(result: string): void {
    this.ensureResultArea(result);
  }

  destroy(): void {
    this.resultArea = null;
    this.runBtn = null;
  }
}