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