1/**
2 * DokuWiki Plugin Mizar Verifiable Docs (View Screen Script)
3 *
4 */
5
6// 必要なモジュールをインポート
7import { EditorState, Compartment, StateEffect, StateField, RangeSetBuilder } from "@codemirror/state";
8import { EditorView, lineNumbers, showPanel, Decoration, ViewPlugin, keymap } from "@codemirror/view";
9import { defaultKeymap, insertNewlineAndIndent } from "@codemirror/commands";
10import {
11  LRLanguage,
12  LanguageSupport,
13  syntaxHighlighting,
14  HighlightStyle,
15  syntaxTree,
16  foldNodeProp,
17  foldInside,
18  foldGutter,
19  foldKeymap,
20  codeFolding,
21  indentNodeProp,
22} from "@codemirror/language"; // ★ codeFoldingなど
23import { parser } from "./mizar-parser.js";
24import { tags as t } from "@lezer/highlight";
25import { highlighting } from "./highlight.js"; // highlight.js からインポート
26
27// ショートカット定義
28const myKeymap = [
29  ...defaultKeymap, // 基本的なキー操作(例: Ctrl+Z など)を利用
30  ...foldKeymap,
31  { key: "Enter", run: insertNewlineAndIndent }, // Enterで改行+インデント
32];
33
34// -----------------------------
35// スタイルの定義
36// -----------------------------
37const highlightStyle = HighlightStyle.define([
38  { tag: t.controlKeyword, class: "control-keyword" },
39  { tag: t.function(t.keyword), class: "function-keyword" },
40  { tag: t.keyword, class: "general-keyword" },
41  { tag: t.typeName, class: "type-name" },
42  { tag: t.meta, class: "meta-info" },
43  { tag: t.lineComment, class: "line-comment" },
44  { tag: t.paren, class: "paren" },
45  { tag: t.brace, class: "brace" },
46  { tag: t.squareBracket, class: "square-bracket" },
47]);
48
49// -----------------------------
50// パーサー設定: 折りたたみ可能なノードを指定
51// -----------------------------
52let parserWithMetadata = parser.configure({
53  props: [
54    highlighting,
55    foldNodeProp.add({
56      DefinitionBlock: foldInside,
57      Proof: foldInside,
58      NowBlock: foldInside,
59      HerebyBlock: foldInside,
60    }),
61  ],
62});
63
64// -----------------------------
65// 言語サポートの定義
66// -----------------------------
67const mizarLanguage = LRLanguage.define({
68  parser: parserWithMetadata,
69});
70
71// -----------------------------
72// インデントの定義
73// -----------------------------
74const myIndent = indentNodeProp.add({
75  // "Proof" ノードが来たら親のインデント +1 段
76  Proof(context) {
77    return context.lineIndent(context.node.from) + context.unit;
78  },
79  DefinitionBlock(context) {
80    return context.lineIndent(context.node.from) + context.unit;
81  },
82  NowBlock(context) {
83    return context.lineIndent(context.node.from) + context.unit;
84  },
85  HerebyBlock(context) {
86    return context.lineIndent(context.node.from) + context.unit;
87  }
88});
89
90// インデント込みの拡張 (mizarWithIndent)
91const mizarWithIndent = mizarLanguage.configure({
92  props: [myIndent],
93});
94
95// ★必要であれば、mizarWithIndent を直接使うか、
96// 下記のように mizar() 関数内で呼び出して使ってもOK
97
98// mizar() は インデント拡張なしのバージョン
99export function mizar() {
100  return new LanguageSupport(mizarLanguage);
101}
102
103// -----------------------------
104// ブラケット強調のプラグイン (色分けなど)
105// -----------------------------
106function bracketHighlighter() {
107  return ViewPlugin.fromClass(
108    class {
109      constructor(view) {
110        this.decorations = this.buildDecorations(view);
111      }
112
113      update(update) {
114        if (update.docChanged || update.viewportChanged) {
115          this.decorations = this.buildDecorations(update.view);
116        }
117      }
118
119      buildDecorations(view) {
120        const builder = new RangeSetBuilder();
121        const stack = [];
122        const colors = [
123          "bracket-color-0",
124          "bracket-color-1",
125          "bracket-color-2",
126          "bracket-color-3",
127          "bracket-color-4",
128        ];
129
130        syntaxTree(view.state).iterate({
131          enter: ({ type, from, to }) => {
132            if (
133              type.is("OpenParen") ||
134              type.is("OpenBrace") ||
135              type.is("OpenBracket")
136            ) {
137              stack.push({ type, from, to });
138              const level = stack.length % colors.length;
139              builder.add(from, to, Decoration.mark({ class: colors[level] }));
140            } else if (
141              type.is("CloseParen") ||
142              type.is("CloseBrace") ||
143              type.is("CloseBracket")
144            ) {
145              const open = stack.pop();
146              if (open) {
147                const level = (stack.length + 1) % colors.length;
148                builder.add(from, to, Decoration.mark({ class: colors[level] }));
149              }
150            }
151          },
152        });
153        return builder.finish();
154      }
155    },
156    {
157      decorations: (v) => v.decorations,
158    }
159  );
160}
161
162// -----------------------------
163// Compartment, editors,などのグローバル管理
164// -----------------------------
165const editableCompartment = new Compartment(); // 共有
166const editors = (window.editors = window.editors || {});
167const lineNumberConfigs = (window.lineNumberConfigs = window.lineNumberConfigs || {});
168const editorOrder = (window.editorOrder = window.editorOrder || []);
169
170// エラーデコレーション用
171const errorDecorationEffect = StateEffect.define();
172const errorDecorationsField = StateField.define({
173  create: () => Decoration.none,
174  update(decorations, tr) {
175    decorations = decorations.map(tr.changes);
176    for (let effect of tr.effects) {
177      if (effect.is(errorDecorationEffect)) {
178        decorations = decorations.update({ add: effect.value });
179      }
180    }
181    return decorations;
182  },
183  provide: (f) => EditorView.decorations.from(f),
184});
185
186// -----------------------------
187// Hide/Show 関連
188// -----------------------------
189function toggleMizarEditor(wrapper, hide) {
190  const elementsToToggle = wrapper.querySelectorAll(
191    ".editor-container, .copy-button, .edit-button, .reset-button, .compile-button"
192  );
193  elementsToToggle.forEach((el) => {
194    el.style.display = hide ? "none" : "";
195  });
196
197  const hideButton = wrapper.querySelector(".hide-button");
198  const showButton = wrapper.querySelector(".show-button");
199  if (hideButton && showButton) {
200    hideButton.style.display = hide ? "none" : "inline";
201    showButton.style.display = hide ? "inline" : "none";
202  }
203}
204
205// -----------------------------
206// DOMContentLoaded時の初期化
207// -----------------------------
208document.addEventListener(
209  "DOMContentLoaded",
210  function () {
211    window.mizarInitialized = true;
212
213    const wrappers = document.querySelectorAll(".mizarWrapper");
214    if (wrappers.length === 0) {
215      console.warn("No Mizar blocks found.");
216      return;
217    }
218
219    wrappers.forEach((block) => {
220      const mizarId = block.id.replace("mizarBlock", "");
221      if (!block.querySelector(".editor-container").firstChild) {
222        setupMizarBlock(block, mizarId);
223      }
224    });
225
226    // Hide/Show ボタン
227    const hideButtons = document.querySelectorAll(".hide-button");
228    hideButtons.forEach((button) => {
229      button.addEventListener("click", () => {
230        const parentWrapper = button.closest(".mizarWrapper");
231        if (!parentWrapper) return;
232        toggleMizarEditor(parentWrapper, true);
233      });
234    });
235
236    const showButtons = document.querySelectorAll(".show-button");
237    showButtons.forEach((button) => {
238      button.addEventListener("click", () => {
239        const parentWrapper = button.closest(".mizarWrapper");
240        if (!parentWrapper) return;
241        toggleMizarEditor(parentWrapper, false);
242      });
243    });
244
245    // Hide All, Show All
246    const hideAllButton = document.getElementById("hideAllButton");
247    const showAllButton = document.getElementById("showAllButton");
248    const resetAllButton = document.getElementById("resetAllButton");
249
250    if (hideAllButton && showAllButton) {
251      hideAllButton.addEventListener("click", (e) => {
252        toggleAllWrappers(true);
253        hideAllButton.style.display = "none";
254        showAllButton.style.display = "";
255        e.target.blur();
256      });
257
258      showAllButton.addEventListener("click", (e) => {
259        toggleAllWrappers(false);
260        showAllButton.style.display = "none";
261        hideAllButton.style.display = "";
262        e.target.blur();
263      });
264    }
265
266    function toggleAllWrappers(hide) {
267      const allWrappers = document.querySelectorAll(".mizarWrapper");
268      allWrappers.forEach((wrapper) => {
269        toggleMizarEditor(wrapper, hide);
270      });
271    }
272
273    // Reset All ボタン
274    if (resetAllButton) {
275      resetAllButton.addEventListener("click", (e) => {
276        const allWrappers = document.querySelectorAll(".mizarWrapper");
277        allWrappers.forEach((wrapper) => {
278          const resetBtn = wrapper.querySelector("button[id^='resetButton']");
279          if (resetBtn) {
280            resetBtn.click();
281          }
282        });
283        e.target.blur();
284      });
285    }
286  },
287  { once: true }
288);
289
290// -----------------------------
291// エラー表示パネル
292// -----------------------------
293const toggleErrorPanel = StateEffect.define();
294const errorPanelState = StateField.define({
295  create: () => null,
296  update(value, tr) {
297    for (let e of tr.effects) {
298      if (e.is(toggleErrorPanel)) value = e.value;
299    }
300    return value;
301  },
302  provide: (f) => showPanel.from(f, (val) => (val ? createPanel(val) : null)),
303});
304
305function createPanel(content) {
306  return (_view) => {
307    let dom = document.createElement("div");
308    dom.innerHTML = content;
309    dom.className = "cm-error-panel";
310    return { dom };
311  };
312}
313
314function calculateStartLineNumber(targetMizarId) {
315  let totalLines = 1;
316  for (let mizarId of editorOrder) {
317    if (mizarId === targetMizarId) {
318      break;
319    }
320    totalLines += editors[mizarId].state.doc.lines;
321  }
322  return totalLines;
323}
324
325function adjustLineNumbersForAllEditors() {
326  let totalLines = 1;
327  for (let mizarId of editorOrder) {
328    const editor = editors[mizarId];
329    const startLine = totalLines;
330    const lineNumberExtension = lineNumbers({
331      formatNumber: (number) => `${number + startLine - 1}`,
332    });
333    editor.dispatch({
334      effects: lineNumberConfigs[mizarId].reconfigure(lineNumberExtension),
335    });
336    totalLines += editor.state.doc.lines;
337  }
338}
339
340function getEditorForGlobalLine(globalLine) {
341  let currentLine = 1;
342  for (let mizarId of editorOrder) {
343    const editor = editors[mizarId];
344    const editorLines = editor.state.doc.lines;
345    if (globalLine >= currentLine && globalLine < currentLine + editorLines) {
346      return { editor, localLine: globalLine - currentLine + 1 };
347    }
348    currentLine += editorLines;
349  }
350  return null;
351}
352
353// -----------------------------
354// エディタの初期設定
355// -----------------------------
356function setupMizarBlock(mizarBlock, mizarId) {
357  const editorContainer = mizarBlock.querySelector(".editor-container");
358  editorContainer.id = `editorContainer${mizarId}`;
359  const editorContent = editorContainer.getAttribute("data-content");
360
361  const tempDiv = document.createElement("div");
362  tempDiv.innerHTML = editorContent;
363  const decodedContent = tempDiv.textContent || tempDiv.innerText || "";
364
365  const lineNumberConfig = new Compartment();
366  lineNumberConfigs[mizarId] = lineNumberConfig;
367
368  // codeFolding()を追加して折りたたみを有効化
369  // ※ここで mizarWithIndent を使いたい場合は差し替えます
370  const editor = new EditorView({
371    state: EditorState.create({
372      doc: decodedContent,
373      extensions: [
374        lineNumberConfig.of(
375          lineNumbers({
376            formatNumber: (number) =>
377              `${number + calculateStartLineNumber(mizarId) - 1}`,
378          })
379        ),
380        editableCompartment.of(EditorView.editable.of(false)),
381        syntaxHighlighting(highlightStyle),
382        codeFolding(), // ★ 折りたたみ
383        foldGutter(),
384        keymap.of(myKeymap), // ← キーマップを登録
385        bracketHighlighter(),
386        EditorView.updateListener.of((update) => {
387          if (update.docChanged) {
388            adjustLineNumbersForAllEditors();
389          }
390        }),
391        errorPanelState,
392        errorDecorationsField,
393        mizarWithIndent,
394      ],
395    }),
396    parent: editorContainer,
397  });
398
399  window.editors[mizarId] = editor;
400  editorOrder.push(mizarId);
401
402  // ボタン系
403  const editButton = mizarBlock.querySelector("button[id^='editButton']");
404  const compileButton = mizarBlock.querySelector("button[id^='compileButton']");
405  const resetButton = mizarBlock.querySelector("button[id^='resetButton']");
406  const graphButton = mizarBlock.querySelector("button[id^='graphButton']");
407
408  editButton.addEventListener("click", () => {
409    editor.dispatch({
410      effects: editableCompartment.reconfigure(EditorView.editable.of(true)),
411    });
412    editButton.style.display = "none";
413    compileButton.style.display = "inline";
414    resetButton.style.display = "inline";
415  });
416
417  compileButton.addEventListener("click", () => {
418      if (mizarBlock.isRequestInProgress) {
419          return;
420      }
421      mizarBlock.isRequestInProgress = true;
422
423      // Spinnerを表示 (この処理を追加)
424      const spinner = document.createElement('div');
425      spinner.className = 'loading-spinner';
426      spinner.innerHTML = 'Loading...';
427      compileButton.parentNode.insertBefore(spinner, compileButton.nextSibling);
428
429      startMizarCompilation(mizarBlock, toggleErrorPanel, mizarId).finally(() => {
430          // Spinnerを削除 (レスポンス後に削除)
431          spinner.remove();
432          mizarBlock.isRequestInProgress = false;
433      });
434  });
435
436  resetButton.addEventListener("click", async () => {
437    const initialContent = editorContainer.getAttribute("data-content");
438    const tempDiv = document.createElement("div");
439    tempDiv.innerHTML = initialContent;
440    const decodedContent = tempDiv.textContent || tempDiv.innerText || "";
441
442    editor.dispatch({
443      changes: { from: 0, to: editor.state.doc.length, insert: decodedContent },
444    });
445
446    editor.dispatch({
447      effects: editableCompartment.reconfigure(EditorView.editable.of(false)),
448    });
449
450    editButton.style.display = "inline";
451    compileButton.style.display = "none";
452    resetButton.style.display = "none";
453
454    editor.dispatch({
455      effects: toggleErrorPanel.of(null),
456    });
457    editor.dispatch({
458      effects: errorDecorationEffect.of([]),
459    });
460
461    try {
462      const response = await fetch(
463        DOKU_BASE + "lib/exe/ajax.php?call=clear_temp_files",
464        {
465          method: "POST",
466          headers: {
467            "Content-Type": "application/x-www-form-urlencoded",
468          },
469        }
470      );
471
472      const text = await response.text();
473      let data;
474      try {
475        data = JSON.parse(text);
476      } catch (error) {
477        throw new Error("Server returned a non-JSON response: " + text);
478      }
479
480      if (data.success) {
481        console.log(data.message);
482      } else {
483        console.error("Failed to clear temporary files:", data.message);
484      }
485    } catch (error) {
486      console.error("Error clearing temporary files:", error);
487    }
488  });
489
490  graphButton.addEventListener("click", () => {
491    if (mizarBlock.isRequestInProgress) return;
492    mizarBlock.isRequestInProgress = true;
493
494    // スピナー表示(任意)
495    const spinner = document.createElement("div");
496    spinner.className = "loading-spinner";
497    spinner.textContent = "Visualizing...";
498    graphButton.parentNode.insertBefore(spinner, graphButton.nextSibling);
499
500    startGraphVisualization(mizarBlock, mizarId)
501      .finally(() => {
502        spinner.remove();
503        mizarBlock.isRequestInProgress = false;
504      });
505  });
506
507}
508
509// -----------------------------
510// 行スクロール関連
511// -----------------------------
512function scrollToLine(editor, line) {
513  const lineInfo = editor.state.doc.line(line);
514  editor.dispatch({
515    scrollIntoView: { from: lineInfo.from, to: lineInfo.to },
516  });
517}
518
519// -----------------------------
520// コンパイル開始
521// -----------------------------
522async function startMizarCompilation(mizarBlock, toggleErrorPanel, mizarId) {
523  if (mizarBlock.eventSource) {
524    mizarBlock.eventSource.close();
525  }
526
527  const combinedContent = getCombinedContentUntil(mizarBlock);
528  const data = "content=" + encodeURIComponent(combinedContent);
529
530  try {
531    const response = await fetch(DOKU_BASE + "lib/exe/ajax.php?call=view_compile", {
532      method: "POST",
533      headers: {
534        "Content-Type": "application/x-www-form-urlencoded",
535      },
536      body: data,
537    });
538    if (!response.ok) {
539      throw new Error("Network response was not ok");
540    }
541
542    mizarBlock.eventSource = new EventSource(
543      DOKU_BASE + "lib/exe/ajax.php?call=view_sse&" + data
544    );
545
546    mizarBlock.eventSource.onmessage = function (event) {
547      const editor = editors[mizarId];
548      let content = editor.state.field(errorPanelState) || "";
549      content += event.data + "<br>";
550      editor.dispatch({
551        effects: toggleErrorPanel.of(content),
552      });
553
554      if (content.includes("❌")) {
555        editor.dom.querySelector(".cm-error-panel").style.backgroundColor = "#fcc";
556      } else {
557        editor.dom.querySelector(".cm-error-panel").style.backgroundColor =
558          "#ccffcc";
559      }
560    };
561
562    mizarBlock.eventSource.addEventListener("compileErrors", function (event) {
563      try {
564        const errors = JSON.parse(event.data);
565        let errorContent = editors[mizarId].state.field(errorPanelState) || "";
566        const decorationsPerEditor = {};
567
568        errors.forEach(function (error) {
569          const { line, column, message } = error;
570          const link = `<a href="#" class="error-link" data-line="${line}" data-column="${column}">[Ln ${line}, Col ${column}]</a>`;
571          errorContent += `❌ ${message} ${link}<br>`;
572
573          const editorInfo = getEditorForGlobalLine(line);
574          if (editorInfo) {
575            const { editor, localLine } = editorInfo;
576            const lineInfo = editor.state.doc.line(localLine);
577            const from = lineInfo.from + (column - 1);
578            const to = from + 1;
579
580            if (from >= lineInfo.from && to <= lineInfo.to) {
581              const deco = Decoration.mark({
582                class: "error-underline",
583                attributes: {
584                  title: `${message} (Ln ${line}, Col ${column})`,
585                },
586              }).range(from, to);
587              const editorId = Object.keys(editors).find(
588                (key) => editors[key] === editor
589              );
590              if (!decorationsPerEditor[editorId]) {
591                decorationsPerEditor[editorId] = [];
592              }
593              decorationsPerEditor[editorId].push(deco);
594            }
595          }
596        });
597
598        for (let editorId in decorationsPerEditor) {
599          const editor = editors[editorId];
600          const decorations = decorationsPerEditor[editorId];
601          if (decorations.length > 0) {
602            editor.dispatch({
603              effects: errorDecorationEffect.of(decorations.sort((a, b) => a.from - b.from)),
604            });
605          }
606        }
607
608        editors[mizarId].dispatch({
609          effects: [toggleErrorPanel.of(errorContent)],
610        });
611
612        if (errorContent.includes("❌")) {
613          editors[mizarId].dom.querySelector(".cm-error-panel").style.backgroundColor =
614            "#fcc";
615        } else {
616          editors[mizarId].dom.querySelector(".cm-error-panel").style.backgroundColor =
617            "#ccffcc";
618        }
619
620        const errorPanel = editors[mizarId].dom.querySelector(".cm-error-panel");
621        errorPanel.querySelectorAll(".error-link").forEach((link) => {
622          link.addEventListener("click", function (e) {
623            e.preventDefault();
624            const line = parseInt(link.getAttribute("data-line"), 10);
625            const editorInfo = getEditorForGlobalLine(line);
626            if (editorInfo) {
627              const { editor, localLine } = editorInfo;
628              scrollToLine(editor, localLine);
629            }
630          });
631        });
632      } catch (e) {
633        console.error("Failed to parse error data:", e);
634        console.error("Event data:", event.data);
635      }
636    });
637
638    mizarBlock.eventSource.onerror = () => {
639      mizarBlock.eventSource.close();
640      mizarBlock.isRequestInProgress = false;
641      finalizeCompilation(mizarBlock);
642    };
643  } catch (error) {
644    console.error("Fetch error: ", error);
645    mizarBlock.isRequestInProgress = false;
646  }
647}
648//-----------------------------
649// ★ Graph 可視化処理
650//-----------------------------
651async function startGraphVisualization(mizarBlock, mizarId) {
652  const combined = getCombinedContentUntil(mizarBlock);   // 既存関数を再利用
653  const data = "content=" + encodeURIComponent(combined);
654
655  try {
656    const res = await fetch(
657      DOKU_BASE + "lib/exe/ajax.php?call=view_graph",
658      {
659        method: "POST",
660        headers: { "Content-Type": "application/x-www-form-urlencoded" },
661        body: data,
662      }
663    );
664    if (!res.ok) throw new Error("network error");
665
666    // ── JSON を 1 変数で受けて衝突を回避 ──
667    const json = await res.json();
668    const out  = mizarBlock.querySelector(`#outputmizarBlock${mizarId}`);
669
670    if (json.success) {
671      /* ▼ SVG と X ボタンをまとめて挿入 */
672      out.innerHTML =
673        `<div class="graph-wrapper">
674          <button class="close-graph" title="Close">✖</button>
675          ${json.data.svg}
676        </div>`;
677      out.style.display = 'block';
678
679      /* ▼ ボタンにイベントを付与 */
680      out.querySelector('.close-graph').addEventListener('click', () => {
681        out.innerHTML = '';
682        out.style.display = 'none';
683      });
684    } else {
685      out.innerHTML = `<pre class="err">${json.data.err}</pre>`;
686      out.style.display = 'block';
687    }
688
689  } catch (e) {
690    console.error(e);
691    alert("Graph generation failed: " + e.message);
692  }
693}
694
695function getCombinedContentUntil(mizarBlock) {
696  let combinedContent = "";
697  const mizarBlocks = document.querySelectorAll(".mizarWrapper");
698  const blockIndex = Array.from(mizarBlocks).indexOf(mizarBlock);
699
700  for (let i = 0; i <= blockIndex; i++) {
701    const mizarId = editorOrder[i];
702    const editor = editors[mizarId];
703    if (editor && editor.state) {
704      const blockContent = editor.state.doc.toString();
705      combinedContent += blockContent + "\n";
706    }
707  }
708  return combinedContent.trim();
709}
710
711function finalizeCompilation(mizarBlock) {
712  const compileButton = mizarBlock.querySelector("[id^='compileButton']");
713  const resetButton = mizarBlock.querySelector("[id^='resetButton']");
714
715  compileButton.style.display = "none";
716  resetButton.style.display = "inline-block";
717  mizarBlock.isRequestInProgress = false;
718}
719
720// -----------------------------
721// ファイル作成関連
722// -----------------------------
723window.createMizarFile = async function (filename) {
724  const combinedContent = collectMizarContents();
725  if (!combinedContent) {
726    console.error("Error: Combined content is empty.");
727    return;
728  }
729  if (!filename.endsWith(".miz")) {
730    filename += ".miz";
731  }
732
733  try {
734    const response = await fetch(DOKU_BASE + "lib/exe/ajax.php?call=create_combined_file", {
735      method: "POST",
736      headers: {
737        "Content-Type": "application/x-www-form-urlencoded",
738      },
739      body:
740        "content=" +
741        encodeURIComponent(combinedContent) +
742        "&filename=" +
743        encodeURIComponent(filename),
744    });
745
746    if (!response.ok) {
747      console.error("Failed to create file: Network response was not ok");
748      return;
749    }
750
751    const data = await response.json();
752    if (data.success) {
753      console.log("File created successfully:", filename);
754
755      const blob = new Blob([data.data.content], { type: "application/octet-stream" });
756      const url = URL.createObjectURL(blob);
757
758      const contentWindow = window.open("", "_blank");
759      contentWindow.document.write(
760        "<pre>" +
761          data.data.content
762            .replace(/&/g, "&amp;")
763            .replace(/</g, "&lt;")
764            .replace(/>/g, "&gt;") +
765          "</pre>"
766      );
767      contentWindow.document.title = filename;
768
769      const downloadLink = contentWindow.document.createElement("a");
770      downloadLink.href = url;
771      downloadLink.download = filename;
772      downloadLink.textContent = "⬇️ Click here to download the file";
773      downloadLink.style.display = "block";
774      downloadLink.style.marginTop = "10px";
775      contentWindow.document.body.appendChild(downloadLink);
776
777      contentWindow.addEventListener("unload", () => {
778        URL.revokeObjectURL(url);
779      });
780    } else {
781      console.error("Failed to create file:", data.message);
782    }
783  } catch (error) {
784    console.error("Error creating file:", error);
785  }
786};
787
788function collectMizarContents() {
789  let combinedContent = "";
790  for (let mizarId of editorOrder) {
791    const editor = editors[mizarId];
792    if (editor && editor.state) {
793      combinedContent += editor.state.doc.toString() + "\n";
794    }
795  }
796  return combinedContent;
797}
798
799// -----------------------------
800// Copyボタンの実装
801// -----------------------------
802document.addEventListener("DOMContentLoaded", () => {
803  const copyButtons = document.querySelectorAll(".copy-button");
804  copyButtons.forEach((button) => {
805    button.addEventListener("click", (event) => {
806      const buttonElement = event.currentTarget;
807      const mizarIdRaw = buttonElement.dataset.mizarid;
808      const mizarId = mizarIdRaw.replace("mizarBlock", "");
809
810      if (!editors[mizarId]) {
811        console.error("エディタが見つかりません: ", mizarIdRaw);
812        return;
813      }
814
815      const editor = editors[mizarId];
816      const content = editor.state.doc.toString();
817
818      navigator.clipboard
819        .writeText(content)
820        .then(() => {
821          buttonElement.textContent = "Copied!";
822          buttonElement.disabled = true;
823
824          setTimeout(() => {
825            buttonElement.textContent = "Copy";
826            buttonElement.disabled = false;
827          }, 2000);
828        })
829        .catch((err) => {
830          console.error("コピーに失敗しました: ", err);
831        });
832    });
833  });
834});
835