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, "&") 763 .replace(/</g, "<") 764 .replace(/>/g, ">") + 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