1"use strict;" 2// 必要なモジュールをインポート 3import { EditorState, Compartment, StateEffect, StateField, RangeSetBuilder } from "@codemirror/state"; 4import { EditorView, lineNumbers, showPanel, Decoration, ViewPlugin } from "@codemirror/view"; 5import { LRLanguage, LanguageSupport, syntaxHighlighting, HighlightStyle, syntaxTree } from "@codemirror/language"; 6import { parser } from "./mizar-parser.js"; 7import { tags as t } from "@lezer/highlight"; 8import { highlighting } from "./highlight.js"; // highlight.js からインポート 9 10// スタイルの定義 11const highlightStyle = HighlightStyle.define([ 12 { tag: t.controlKeyword, class: "control-keyword" }, // 制御キーワード 13 { tag: t.function(t.keyword), class: "function-keyword" }, // サポート関数 14 { tag: t.keyword, class: "general-keyword" }, // 一般的なキーワード 15 { tag: t.typeName, class: "type-name" }, // 型名やエンティティ名 16 { tag: t.meta, class: "meta-info" }, // メタ情報(推論句) 17 { tag: t.lineComment, class: "line-comment" }, // 行コメント 18 { tag: t.paren, class: "paren" }, // 括弧 19 { tag: t.brace, class: "brace" }, // 中括弧 20 { tag: t.squareBracket, class: "square-bracket" }, // 角括弧 21]); 22 23// パーサーの設定 24let parserWithMetadata = parser.configure({ 25 props: [ 26 highlighting // highlight.js からインポートした highlighting を使用 27 ] 28}); 29 30// 言語サポートの定義 31const mizarLanguage = LRLanguage.define({ 32 parser: parserWithMetadata 33}); 34 35function mizar() { 36 return new LanguageSupport(mizarLanguage); 37} 38 39function bracketHighlighter() { 40 return ViewPlugin.fromClass(class { 41 constructor(view) { 42 this.decorations = this.buildDecorations(view); 43 } 44 45 update(update) { 46 if (update.docChanged || update.viewportChanged) { 47 this.decorations = this.buildDecorations(update.view); 48 } 49 } 50 51 buildDecorations(view) { 52 const builder = new RangeSetBuilder(); 53 const stack = []; 54 const colors = ['bracket-color-0', 'bracket-color-1', 'bracket-color-2', 'bracket-color-3', 'bracket-color-4']; 55 56 syntaxTree(view.state).iterate({ 57 enter: ({ type, from, to }) => { 58 if (type.is('OpenParen') || type.is('OpenBrace') || type.is('OpenBracket')) { 59 stack.push({ type, from, to }); 60 const level = stack.length % colors.length; 61 builder.add(from, to, Decoration.mark({ class: colors[level] })); 62 } else if (type.is('CloseParen') || type.is('CloseBrace') || type.is('CloseBracket')) { 63 const open = stack.pop(); 64 if (open) { 65 const level = (stack.length + 1) % colors.length; 66 builder.add(from, to, Decoration.mark({ class: colors[level] })); 67 } 68 } 69 } 70 }); 71 72 return builder.finish(); 73 } 74 }, { 75 decorations: v => v.decorations 76 }); 77 } 78 79const editableCompartment = new Compartment(); // 共有Compartment 80// グローバルオブジェクトとしてエディタを管理 81const editors = window.editors = window.editors || {}; 82const lineNumberConfigs = window.lineNumberConfigs = window.lineNumberConfigs || {}; 83const editorOrder = window.editorOrder = window.editorOrder || []; 84 85// エラーデコレーションのエフェクトと状態フィールドを定義 86const errorDecorationEffect = StateEffect.define(); 87const errorDecorationsField = StateField.define({ 88 create: () => Decoration.none, 89 update(decorations, tr) { 90 decorations = decorations.map(tr.changes); 91 for (let effect of tr.effects) { 92 if (effect.is(errorDecorationEffect)) { 93 decorations = decorations.update({ add: effect.value }); 94 } 95 } 96 return decorations; 97 }, 98 provide: f => EditorView.decorations.from(f) 99}); 100 101// DOMContentLoaded時の初期化 102document.addEventListener("DOMContentLoaded", function () { 103 window.mizarInitialized = true; 104 105 const wrappers = document.querySelectorAll('.mizarWrapper'); 106 if (wrappers.length === 0) { 107 console.warn("No Mizar blocks found."); 108 return; 109 } 110 111 wrappers.forEach((block) => { 112 const mizarId = block.id.replace('mizarBlock', ''); 113 if (!block.querySelector('.editor-container').firstChild) { 114 setupMizarBlock(block, mizarId); 115 } 116 }); 117}, { once: true }); // イベントリスナーを一度だけ実行 118 119// パネルの表示を制御するエフェクトとステートフィールド 120const toggleErrorPanel = StateEffect.define(); 121const errorPanelState = StateField.define({ 122 create: () => null, 123 update(value, tr) { 124 for (let e of tr.effects) { 125 if (e.is(toggleErrorPanel)) value = e.value; 126 } 127 return value; 128 }, 129 provide: f => showPanel.from(f, val => val ? createPanel(val) : null) 130}); 131 132function createPanel(content) { 133 return (_view) => { 134 let dom = document.createElement("div"); 135 dom.innerHTML = content; 136 dom.className = "cm-error-panel"; 137 return { dom }; 138 }; 139} 140 141function calculateStartLineNumber(targetMizarId) { 142 let totalLines = 1; // 最初の行を1に変更 143 for (let mizarId of editorOrder) { 144 if (mizarId === targetMizarId) { 145 break; 146 } 147 totalLines += editors[mizarId].state.doc.lines; 148 } 149 return totalLines; // ここで+1を追加しない 150} 151 152function adjustLineNumbersForAllEditors() { 153 let totalLines = 1; // 最初の行を1に変更 154 for (let mizarId of editorOrder) { 155 const editor = editors[mizarId]; 156 const startLine = totalLines; 157 const lineNumberExtension = lineNumbers({ 158 formatNumber: number => `${number + startLine - 1}` 159 }); 160 // Compartmentのreconfigureメソッドを使用して設定を更新 161 editor.dispatch({ 162 effects: lineNumberConfigs[mizarId].reconfigure(lineNumberExtension) 163 }); 164 // 次のエディタの開始行番号のために行数を加算 165 totalLines += editor.state.doc.lines; 166 } 167} 168 169function getEditorForGlobalLine(globalLine) { 170 let currentLine = 1; 171 for (let mizarId of editorOrder) { 172 const editor = editors[mizarId]; 173 const editorLines = editor.state.doc.lines; 174 if (globalLine >= currentLine && globalLine < currentLine + editorLines) { 175 return { editor, localLine: globalLine - currentLine + 1 }; 176 } 177 currentLine += editorLines; 178 } 179 return null; 180} 181 182// エディタの初期設定時にリスナーを追加 183function setupMizarBlock(mizarBlock, mizarId) { 184 const editorContainer = mizarBlock.querySelector('.editor-container'); 185 editorContainer.id = `editorContainer${mizarId}`; 186 const editorContent = editorContainer.getAttribute('data-content'); 187 188 // テンポラリDOM要素を作成してinnerHTMLに設定することでデコードを実行 189 const tempDiv = document.createElement('div'); 190 tempDiv.innerHTML = editorContent; 191 const decodedContent = tempDiv.textContent || tempDiv.innerText || ""; 192 193 const lineNumberConfig = new Compartment(); 194 lineNumberConfigs[mizarId] = lineNumberConfig; // 各エディタの設定を保持 195 196 // EditorViewの初期化部分 197 const editor = new EditorView({ 198 state: EditorState.create({ 199 doc: decodedContent, // デコードされたコンテンツを使用 200 extensions: [ 201 lineNumberConfig.of(lineNumbers({ formatNumber: number => `${number + calculateStartLineNumber(mizarId) - 1}` })), 202 // EditorCustomThemeは削除 203 editableCompartment.of(EditorView.editable.of(false)), // 初期状態を読み取り専用に設定 204 syntaxHighlighting(highlightStyle), // 追加: ハイライトスタイルの適用 205 bracketHighlighter(), 206 EditorView.updateListener.of(update => { 207 if (update.docChanged) { 208 adjustLineNumbersForAllEditors(); // すべてのエディタの行番号を更新 209 } 210 }), 211 errorPanelState, 212 errorDecorationsField, 213 syntaxHighlighting(highlightStyle), 214 mizar() // Mizarの言語サポートを追加 215 ] 216 }), 217 parent: editorContainer 218 }); 219 window.editors[mizarId] = editor; // エディタをオブジェクトに追加 220 editorOrder.push(mizarId); // エディタのIDを順序付けて保持 221 222 const editButton = mizarBlock.querySelector('button[id^="editButton"]'); 223 const mizfButton = mizarBlock.querySelector('button[id^="mizfButton"]'); 224 const resetButton = mizarBlock.querySelector('button[id^="resetButton"]'); 225 226 editButton.addEventListener('click', () => { 227 editor.dispatch({ 228 effects: editableCompartment.reconfigure(EditorView.editable.of(true)) // 編集可能に設定 229 }); 230 editButton.style.display = 'none'; 231 mizfButton.style.display = 'inline'; // 検証ボタンを表示 232 resetButton.style.display = 'inline'; // resetボタンも表示 233 }); 234 235 mizfButton.addEventListener('click', () => { 236 if (mizarBlock.isRequestInProgress) { 237 return; // すでにこのブロックでリクエストが進行中 238 } 239 mizarBlock.isRequestInProgress = true; // リクエスト開始をマーク 240 startMizarCompilation(mizarBlock, toggleErrorPanel, mizarId); 241 }); 242 243 resetButton.addEventListener('click', async () => { 244 // エディタを最初の状態に戻す(Wiki本体に保存されている文書を再読み込み) 245 const initialContent = editorContainer.getAttribute('data-content'); // 初期コンテンツを再取得 246 const tempDiv = document.createElement('div'); // HTMLエンティティのデコード用 247 tempDiv.innerHTML = initialContent; 248 const decodedContent = tempDiv.textContent || tempDiv.innerText || ""; 249 250 editor.dispatch({ 251 changes: { from: 0, to: editor.state.doc.length, insert: decodedContent } // 初期コンテンツを挿入 252 }); 253 254 editor.dispatch({ 255 effects: editableCompartment.reconfigure(EditorView.editable.of(false)) // 読み取り専用に戻す 256 }); 257 258 editButton.style.display = 'inline'; // 編集ボタンを再表示 259 mizfButton.style.display = 'none'; // 検証ボタンを非表示 260 resetButton.style.display = 'none'; // クリアボタンを非表示 261 262 // エラーパネルを非表示にする 263 editor.dispatch({ 264 effects: toggleErrorPanel.of(null) // エラーパネルを非表示に設定 265 }); 266 267 // エラーデコレーションも削除する 268 editor.dispatch({ 269 effects: errorDecorationEffect.of([]) // エラーデコレーションをリセット 270 }); 271 272 try { 273 const response = await fetch(DOKU_BASE + "lib/exe/ajax.php?call=clear_temp_files", { 274 method: "POST", 275 headers: { 276 "Content-Type": "application/x-www-form-urlencoded" 277 } 278 }); 279 280 // JSONで返ってくることを期待する 281 const text = await response.text(); 282 let data; 283 try { 284 data = JSON.parse(text); 285 } catch (error) { 286 // JSONのパースに失敗した場合はHTMLレスポンスだと判断 287 throw new Error("Server returned a non-JSON response: " + text); 288 } 289 290 if (data.success) { 291 console.log(data.message); 292 } else { 293 console.error("Failed to clear temporary files:", data.message); 294 } 295 } catch (error) { 296 console.error("Error clearing temporary files:", error); 297 } 298 }); 299 300 // Enterキーで行を増やすリスナーを追加 301 editor.dom.addEventListener('keydown', (e) => { 302 if (e.key === 'Enter' && editor.state.facet(EditorView.editable)) { 303 e.preventDefault(); 304 const transaction = editor.state.update({ 305 changes: { from: editor.state.selection.main.head, insert: '\n' } 306 }); 307 editor.dispatch(transaction); 308 } 309 }); 310} 311 312function scrollToLine(editor, line) { 313 const lineInfo = editor.state.doc.line(line); 314 editor.dispatch({ 315 scrollIntoView: { from: lineInfo.from, to: lineInfo.to } 316 }); 317} 318 319async function startMizarCompilation(mizarBlock, toggleErrorPanel, mizarId) { 320 // 既存のイベントソースがあれば閉じる 321 if (mizarBlock.eventSource) { 322 mizarBlock.eventSource.close(); 323 } 324 325 const combinedContent = getCombinedContentUntil(mizarBlock); 326 const data = "content=" + encodeURIComponent(combinedContent); 327 328 try { 329 const response = await fetch(DOKU_BASE + "lib/exe/ajax.php?call=view_compile", { 330 method: "POST", 331 headers: { 332 "Content-Type": "application/x-www-form-urlencoded" 333 }, 334 body: data 335 }); 336 337 if (!response.ok) { 338 throw new Error("Network response was not ok"); 339 } 340 341 mizarBlock.eventSource = new EventSource(DOKU_BASE + "lib/exe/ajax.php?call=view_sse&" + data); 342 343 mizarBlock.eventSource.onmessage = function(event) { 344 const editor = editors[mizarId]; 345 let content = editor.state.field(errorPanelState) || ''; 346 content += event.data + '<br>'; 347 editor.dispatch({ 348 effects: toggleErrorPanel.of(content) 349 }); 350 351 // エラーメッセージが含まれているかどうかをチェックしてパネルの背景色を設定 352 if (content.includes('❌')) { 353 editor.dom.querySelector('.cm-error-panel').style.backgroundColor = '#fcc'; 354 } else { 355 editor.dom.querySelector('.cm-error-panel').style.backgroundColor = '#ccffcc'; 356 } 357 }; 358 359 // コンパイルが完了したことを示すイベントを受け取る 360 mizarBlock.eventSource.addEventListener('compileFinished', () => { 361 finalizeCompilation(mizarBlock); 362 }); 363 364 // エラー情報がある場合に受信するイベントリスナー 365 mizarBlock.eventSource.addEventListener('compileErrors', function(event) { 366 try { 367 const errors = JSON.parse(event.data); 368 let errorContent = editors[mizarId].state.field(errorPanelState) || ''; 369 const decorationsPerEditor = {}; // 各エディタのデコレーションを保持 370 371 errors.forEach(function(error) { 372 const { line, column, message } = error; 373 const link = `<a href="#" class="error-link" data-line="${line}" data-column="${column}">[Ln ${line}, Col ${column}]</a>`; 374 errorContent += `❌ ${message} ${link}<br>`; 375 376 // エラー位置に下線を引くデコレーションを追加 377 const editorInfo = getEditorForGlobalLine(line); 378 if (editorInfo) { 379 const { editor, localLine } = editorInfo; 380 const lineInfo = editor.state.doc.line(localLine); 381 const from = lineInfo.from + (column - 1); 382 const to = from + 1; 383 384 if (from >= lineInfo.from && to <= lineInfo.to) { // 範囲が有効であることを確認 385 const deco = Decoration.mark({ 386 class: "error-underline", 387 attributes: { title: `${message} (Ln ${line}, Col ${column})` } // ツールチップの追加 388 }).range(from, to); 389 const editorId = Object.keys(editors).find(key => editors[key] === editor); 390 if (!decorationsPerEditor[editorId]) { 391 decorationsPerEditor[editorId] = []; 392 } 393 decorationsPerEditor[editorId].push(deco); 394 } 395 } 396 }); 397 398 // 各エディタにデコレーションを適用 399 for (let editorId in decorationsPerEditor) { 400 const editor = editors[editorId]; 401 const decorations = decorationsPerEditor[editorId]; 402 if (decorations.length > 0) { 403 editor.dispatch({ 404 effects: errorDecorationEffect.of(decorations.sort((a, b) => a.from - b.from)) 405 }); 406 } 407 } 408 409 // エラーとコンパイル結果を指定ブロックにのみ適用 410 editors[mizarId].dispatch({ 411 effects: [ 412 toggleErrorPanel.of(errorContent) 413 ] 414 }); 415 416 // エラーメッセージが含まれているかどうかをチェックしてパネルの背景色を設定 417 if (errorContent.includes('❌')) { 418 editors[mizarId].dom.querySelector('.cm-error-panel').style.backgroundColor = '#fcc'; 419 } else { 420 editors[mizarId].dom.querySelector('.cm-error-panel').style.backgroundColor = '#ccffcc'; 421 } 422 423 // エラーメッセージ内のリンクにクリックイベントを追加 424 const errorPanel = editors[mizarId].dom.querySelector('.cm-error-panel'); 425 errorPanel.querySelectorAll('.error-link').forEach(link => { 426 link.addEventListener('click', function(e) { 427 e.preventDefault(); 428 const line = parseInt(link.getAttribute('data-line'), 10); 429 const editorInfo = getEditorForGlobalLine(line); 430 if (editorInfo) { 431 const { editor, localLine } = editorInfo; 432 scrollToLine(editor, localLine); 433 } 434 }); 435 }); 436 } catch (e) { 437 console.error('Failed to parse error data:', e); 438 console.error('Event data:', event.data); 439 } 440 }); 441 442 mizarBlock.eventSource.onerror = () => { 443 mizarBlock.eventSource.close(); 444 mizarBlock.isRequestInProgress = false; 445 finalizeCompilation(mizarBlock); // コンパイル処理が完了した後の処理 446 }; 447 } catch (error) { 448 console.error("Fetch error: ", error); 449 mizarBlock.isRequestInProgress = false; 450 } 451} 452 453function getCombinedContentUntil(mizarBlock) { 454 let combinedContent = ''; 455 const mizarBlocks = document.querySelectorAll('.mizarWrapper'); 456 const blockIndex = Array.from(mizarBlocks).indexOf(mizarBlock); 457 458 // 指定したブロックまでのコンテンツを結合 459 for (let i = 0; i <= blockIndex; i++) { 460 const mizarId = editorOrder[i]; 461 const editor = editors[mizarId]; 462 if (editor && editor.state) { 463 const blockContent = editor.state.doc.toString(); 464 combinedContent += blockContent + "\n"; // 正しい順序でコンテンツを追加 465 } 466 } 467 468 return combinedContent.trim(); 469} 470 471function finalizeCompilation(mizarBlock) { 472 // mizarBlock から直接ボタン要素を取得 473 const mizfButton = mizarBlock.querySelector('[id^="mizfButton"]'); 474 const resetButton = mizarBlock.querySelector('[id^="resetButton"]'); 475 476 // mizfButtonを非表示にし、resetButtonを表示 477 mizfButton.style.display = 'none'; 478 resetButton.style.display = 'inline-block'; 479 480 // このブロックのリクエスト進行中状態をリセット 481 mizarBlock.isRequestInProgress = false; 482} 483 484window.createMizarFile = async function(filename) { 485 const combinedContent = collectMizarContents(); 486 487 if (!combinedContent) { 488 console.error('Error: Combined content is empty.'); 489 return; 490 } 491 492 try { 493 const response = await fetch(DOKU_BASE + 'lib/exe/ajax.php?call=create_combined_file', { 494 method: 'POST', 495 headers: { 496 'Content-Type': 'application/x-www-form-urlencoded' 497 }, 498 body: 'content=' + encodeURIComponent(combinedContent) + '&filename=' + encodeURIComponent(filename) 499 }); 500 501 if (!response.ok) { 502 console.error('Failed to create file: Network response was not ok'); 503 return; 504 } 505 506 const data = await response.json(); 507 if (data.success) { 508 console.log('File created successfully:', filename); 509 510 // ファイルの内容を表示する新しいタブを開く 511 const contentWindow = window.open('', '_blank'); 512 contentWindow.document.write(`<pre>${data.data.content}</pre>`); 513 contentWindow.document.title = filename; 514 515 // Blobを使ってダウンロードリンクを作成 516 const blob = new Blob([data.data.content], { type: 'text/plain' }); 517 const url = URL.createObjectURL(blob); 518 519 // ダウンロードリンクの要素を作成して表示 520 const downloadLink = contentWindow.document.createElement('a'); 521 downloadLink.href = url; 522 downloadLink.download = filename; 523 downloadLink.textContent = '⬇️ Click here to download the file'; 524 downloadLink.style.display = 'block'; 525 downloadLink.style.marginTop = '10px'; 526 // タブ内にダウンロードリンクを追加 527 contentWindow.document.body.appendChild(downloadLink); 528 529 // リソースの解放 530 contentWindow.addEventListener('unload', () => { 531 URL.revokeObjectURL(url); 532 }); 533 } else { 534 console.error('Failed to create file:', data.message); 535 } 536 } catch (error) { 537 console.error('Error creating file:', error); 538 } 539}; 540 541function collectMizarContents() { 542 let combinedContent = ''; 543 for (let mizarId of editorOrder) { 544 const editor = editors[mizarId]; 545 if (editor && editor.state) { 546 combinedContent += editor.state.doc.toString() + '\n'; 547 } 548 } 549 return combinedContent; 550} 551 552document.addEventListener("DOMContentLoaded", () => { 553 const copyButtons = document.querySelectorAll('.copy-button'); 554 555 copyButtons.forEach((button) => { 556 button.addEventListener('click', (event) => { 557 const buttonElement = event.currentTarget; 558 const mizarIdRaw = buttonElement.dataset.mizarid; // mizarBlock12 形式を取得 559 const mizarId = mizarIdRaw.replace('mizarBlock', ''); // 数字部分を抽出 560 561 // エディタが存在するか確認 562 if (!editors[mizarId]) { 563 console.error('エディタが見つかりません: ', mizarIdRaw); 564 return; 565 } 566 567 const editor = editors[mizarId]; 568 const content = editor.state.doc.toString(); 569 570 // クリップボードにコピー 571 navigator.clipboard.writeText(content).then(() => { 572 buttonElement.textContent = 'Copied!'; 573 buttonElement.disabled = true; 574 575 setTimeout(() => { 576 buttonElement.textContent = 'Copy'; 577 buttonElement.disabled = false; 578 }, 2000); 579 }).catch((err) => { 580 console.error('コピーに失敗しました: ', err); 581 }); 582 }); 583 }); 584});