1import {Controller} from '@hotwired/stimulus'; 2import Cookies from "../vendor/js.cookie/js.cookie.index.js"; 3import axios from "../vendor/axios/axios.index.js"; 4import LatexDOM from "../lib/latexDOM.js"; 5import notifications from "../lib/notifications.js"; 6import Log from "../lib/log.js"; 7 8/* 9 * This is an example Stimulus controller! 10 * 11 * Any element with a data-controller="hello" attribute will cause 12 * this controller to be executed. The name "hello" comes from the filename: 13 * hello_controller.js -> "hello" 14 * 15 * Delete this file or adapt it for your use! 16 */ 17export default class advancedEditor extends Controller { 18 static values = { 19 render: String, 20 revert: String, 21 save: String, 22 id: String, 23 language: String, 24 searchMode: { type: String, default: 'caseSensitive' }, 25 } 26 static targets = ['search', 'replace', 'searchModeIcon']; 27 28 static searchModes = { 29 caseSensitive: { icon: 'fa-magnifying-glass', caseSensitive: true, regExp: false, wholeWord: false }, 30 caseInsensitive: { icon: 'fa-font', caseSensitive: false, regExp: false, wholeWord: false }, 31 regex: { icon: 'fa-asterisk', caseSensitive: false, regExp: true, wholeWord: false }, 32 wholeWord: { icon: 'fa-spell-check', caseSensitive: true, regExp: false, wholeWord: true }, 33 }; 34 35 setSearchMode(e) { 36 const mode = e.params.mode; 37 if (!advancedEditor.searchModes[mode]) return; 38 this.searchModeValue = mode; 39 const { icon } = advancedEditor.searchModes[mode]; 40 this.searchModeIconTarget.className = `fa-solid ${icon}`; 41 } 42 43 _findOptions() { 44 const { caseSensitive, regExp, wholeWord } = advancedEditor.searchModes[this.searchModeValue] ?? advancedEditor.searchModes.caseSensitive; 45 return { wrap: true, caseSensitive, regExp, wholeWord }; 46 } 47 48 search(e) { 49 let pattern = this.searchTarget.value; 50 let codeEditor = ace.edit("editorCode"); 51 codeEditor.focus(); 52 codeEditor.find(pattern, this._findOptions()); 53 } 54 55 replace(e) { 56 let pattern = this.searchTarget.value; 57 let replace = this.replaceTarget.value; 58 let codeEditor = ace.edit("editorCode"); 59 codeEditor.focus(); 60 codeEditor.find(pattern, this._findOptions()); 61 codeEditor.replace(replace); 62 } 63 64 close(e) { 65 let elementId = this.idValue; 66 let appendId = elementId+'/append'; 67 let element = document.getElementById(appendId); 68 if (element !== null) { 69 element.remove(); 70 Cookies.set('LaTeX2Web_Editor', 'inactive', {path: ''}); 71 document.dispatchEvent(new CustomEvent('editor:deactivated')); 72 } 73 let id = this.idValue; 74 let objectType = LatexDOM.objectType(id); 75 if (LatexDOM.sectionKeywords && !LatexDOM.sectionKeywords.includes(objectType)) { 76 id = LatexDOM.closestParent(id); 77 } 78 let idArray = id.split('/'); 79 let tail = idArray.pop(); 80 let tailArray = tail.split('-'); 81 if (tailArray[0]==='section'){ 82 id+='/div'; 83 } 84 element = document.getElementById(id); 85 if (element !== null) { 86 element.style.backgroundColor = 'white'; 87 element.classList.remove('editing'); 88 if (element.tagName === 'PRE') { 89 hljs.highlightAll(); 90 } 91 } 92 if (LatexDOM.sectionKeywords && LatexDOM.sectionKeywords.includes(objectType)){ 93 let escapedId = elementId.replace(/\//g, '\\/'); 94 let editHeaders = document.querySelectorAll('[id="' + elementId + '"]'); 95 let m = editHeaders.length; 96 var headerElement = null; 97 for (let i = 0; i < m; i++) { 98 headerElement = editHeaders[i]; 99 let headerClasses = headerElement.classList; 100 if (headerClasses.contains('editing')){ 101 headerElement.classList.remove('editing'); 102 } else { 103 headerElement.remove(); 104 } 105 } 106 } 107 let editingObjects = document.getElementsByClassName('editing'); 108 let n = editingObjects.length; 109 for (let i = 0; i < n; i++) { 110 editingObjects[i].remove(); 111 } 112 if (LatexDOM.sectionKeywords && LatexDOM.sectionKeywords.includes(objectType)){ 113 sessionStorage.setItem('elementId', elementId); 114 window.location.reload(); 115 } 116 if (element) { 117 element.scrollIntoView({behavior: "smooth"}); 118 } 119 } 120 121 revert(e) { 122 let elementId = this.idValue; 123 let objectType = LatexDOM.objectType(elementId); 124 let destinationId = elementId; 125 if (LatexDOM.sectionKeywords && !LatexDOM.sectionKeywords.includes(objectType)) { 126 destinationId = LatexDOM.closestParent(elementId); 127 } 128 let controllerRoute = this.revertValue; 129 let language = this.languageValue; 130 var math = document.getElementById(elementId); 131 axios.post(controllerRoute, { 132 'elementId': destinationId, 133 'language': language, 134 }) 135 .then(function (response) { 136 Log.log(response.data); 137 let objectType = LatexDOM.objectType(elementId); 138 if (LatexDOM.sectionKeywords && !LatexDOM.sectionKeywords.includes(objectType)) 139 { 140 math.innerHTML = response.data.html; 141 let children = math.children; 142 let n = children.length; 143 for (let i = 0; i < n; i++) { 144 let child = children[i]; 145 child.style.display = 'block'; 146 } 147 var content = math; 148 } else { 149 let temp = document.createElement('DIV'); 150 temp.innerHTML = response.data.html; 151 let children = temp.children; 152 children[0].classList.add('editing'); 153 math.replaceWith(children[0]); 154 var content = document.getElementById(elementId+'/div'); 155 content.innerHTML = children[0].innerHTML; 156 content.classList.add('editing'); 157 } 158 hljs.highlightAll(); 159 let latex = response.data.latex; 160 let codeEditor = ace.edit("editorCode"); 161 codeEditor.setValue(latex,-1); 162 new Notify({ 163 status: 'success', 164 title: 'Document reverted to saved version', 165 customIcon: '<i class="fa-solid fa-rotate-left"></i></i>', 166 }) 167 let target = document.getElementById(destinationId); 168 if (target) { 169 target.scrollIntoView({behavior: "smooth"}); 170 } 171 let promise = Promise.resolve(); 172 173 function typeset(code) { 174 promise = promise.then(() => MathJax.typesetPromise(code())) 175 .catch((err) => console.log('Typeset failed: ' + err.message)); 176 return promise; 177 } 178 179 typeset(() => { 180 //content.innerHTML = response.data.html; 181 return [content]; 182 }); 183 }) 184 .catch() 185 186 } 187 188 save(e) { 189 var elementId = this.idValue; 190 var destinationId = elementId; 191 let objectType = LatexDOM.objectType(elementId); 192 if (LatexDOM.sectionKeywords && !LatexDOM.sectionKeywords.includes(objectType)) { 193 destinationId = LatexDOM.closestParent(elementId); 194 } 195 let controllerRoute = this.saveValue; 196 let language = this.languageValue; 197 let params = e.params; 198 let action = params.action; 199 let codeEditor = ace.edit("editorCode"); 200 let content = codeEditor.getValue(); 201 new Notify({ 202 status: 'info', 203 title: 'Starting save operation...', 204 autoclose: true, 205 customIcon: '<i class="fa-solid fa-sync fa-spin"></i>', 206 }); 207 axios.post(controllerRoute, { 208 'elementId': destinationId, 209 'latex': content, 210 'language': language, 211 'action': action, 212 }) 213 .then(function (response) { 214 Log.log(response.data); 215 if (response.data.reload) { 216 if(response.data.reload === true){ 217 let id = response.data.id; 218 let message = response.data.message; 219 message = JSON.stringify(message); 220 sessionStorage.setItem('messages', message); 221 let element = document.getElementById(id); 222 if (element !== null) { 223 sessionStorage.setItem('elementId', id); 224 } 225 window.location.reload(); 226 } 227 } else { 228 let type = LatexDOM.objectType(destinationId); 229 let subtypes = advancedEditor.subTypes; 230 let messages = response.data.message; 231 if (subtypes.includes(type)) { 232 sessionStorage.setItem('elementId', elementId); 233 message = JSON.stringify(message); 234 sessionStorage.setItem('messages', messages); 235 window.location.reload(); 236 } else { 237 notifications.notify(messages); 238 } 239 let target = document.getElementById(destinationId); 240 if (target) { 241 target.scrollIntoView({behavior: "smooth"}); 242 } 243 } 244 }) 245 .catch() 246 } 247 248 render(e) { 249 let elementId = this.idValue; 250 let objectType = LatexDOM.objectType(elementId); 251 if (objectType === 'chunk'){ 252 new Notify({ 253 title: 'Chunks cannot be rendered', 254 text: 'You can render the chunk container instead.', 255 status: 'warning', 256 }) 257 return; 258 } 259 var destinationId = elementId; 260 if (LatexDOM.sectionKeywords && !LatexDOM.sectionKeywords.includes(objectType)) { 261 destinationId = LatexDOM.closestParent(elementId); 262 } 263 var math = document.getElementById(elementId); 264 let controllerRoute = this.renderValue; 265 let language = this.languageValue; 266 let codeEditor = ace.edit("editorCode"); 267 let content = codeEditor.getValue(); 268 axios.post(controllerRoute, { 269 'elementId': destinationId, 270 'latex': content, 271 'language': language, 272 }) 273 .then(function (response) { 274 Log.log(response.data); 275 let objectType = LatexDOM.objectType(elementId); 276 if (LatexDOM.sectionKeywords && !LatexDOM.sectionKeywords.includes(objectType)) { 277 math.innerHTML = response.data.html; 278 let children = math.children; 279 let n = children.length; 280 for (let i = 0; i < n; i++) { 281 let child = children[i]; 282 child.style.display = 'block'; 283 } 284 var content = math; 285 } else { 286 let temp = document.createElement('DIV'); 287 temp.innerHTML = response.data.html; 288 let headerId = math.id; 289 let escapedId = headerId.replace(/\//g, '\\/'); 290 let editHeaders = document.querySelectorAll('[id="' + headerId + '"]'); 291 292 let m = editHeaders.length; 293 let found = false;
294 var headerElement = null; 295 for (let i = 0; i < m; i++) { 296 headerElement = editHeaders[i]; 297 let headerClasses = headerElement.classList; 298 if (headerClasses.contains('editing')){ 299 found = true; 300 break; 301 } 302 } 303 if (found){ 304 math = headerElement; 305 } 306 let children = temp.children; 307 math.innerHTML = children[0].innerHTML; 308 var content = document.getElementById(elementId + '/div'); 309 content.innerHTML = children[1].innerHTML; 310 } 311 hljs.highlightAll(); 312 let status = response.data.status; 313 let title = 'Render successful' 314 let icon = '<i class="fa-solid fa-pen-swirl"></i>'; 315 let autoclose = true; 316 let text = ''; 317 if (status !== 'success'){ 318 title = 'Render failed'; 319 autoclose = false; 320 icon = '<i class="fa-solid fa-bomb"></i>' 321 text = response.data.message; 322 } 323 new Notify({ 324 status: status, 325 title: title, 326 autoclose: autoclose, 327 text: text, 328 customIcon: icon, 329 }) 330 let target = document.getElementById(destinationId); 331 if (target) { 332 target.scrollIntoView({behavior: "smooth"}); 333 } 334 let promise = Promise.resolve(); 335 336 function typeset(code) { 337 promise = promise.then(() => MathJax.typesetPromise(code())) 338 .catch((err) => 339 new Notify({ 340 status: 'error', 341 title: 'Typeset failed', 342 text: err.message, 343 })) 344 return promise; 345 } 346 347 typeset(() => { 348 return [content]; 349 }); 350 }) 351 .catch(function (error) { 352 new Notify({ 353 status: 'error', 354 title: error.response.data.message, 355 autoclose: false, 356 customIcon: '<i class="fa-solid fa-bomb"></i>', 357 }) 358 }) 359 } 360 replaceAll(e) { 361 let pattern = this.searchTarget.value; 362 let replace = this.replaceTarget.value; 363 let codeEditor = ace.edit("editorCode"); 364 codeEditor.focus(); 365 let selectedText = codeEditor.getSelectedText(); 366 if (selectedText !== '') { 367 const opts = this._findOptions(); 368 const flags = opts.caseSensitive ? 'g' : 'gi'; 369 let src = opts.regExp ? pattern : pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 370 if (opts.wholeWord) src = '\\b' + src + '\\b'; 371 const newText = selectedText.replace(new RegExp(src, flags), replace); 372 codeEditor.session.replace(codeEditor.selection.getRange(), newText); 373 } else { 374 codeEditor.find(pattern, this._findOptions()); 375 codeEditor.replaceAll(replace); 376 } 377 } 378 markdownToLatex(e){ 379 var codeEditor = ace.edit("editorCode"); 380 var content = codeEditor.getCopyText(); 381 var selection = ''; 382 if (content !== '') { 383 selection = content; 384 codeEditor.remove(); 385 } else { 386 content = codeEditor.getValue(); 387 } 388 let slug = this.getSlug(); 389 axios.post('/advancedEdit/markdownToLatex', { 390 'content': content, 391 'slug': slug, 392 } 393 ) 394 .then((response) => { 395 let latex = response.data.latex.trim(); 396 codeEditor.focus(); 397 if (selection === '') { 398 codeEditor.setValue(latex); 399 } else { 400 codeEditor.insert(latex); 401 } 402 }) 403 .catch(function (error) { 404 alert(error); 405 }) 406 } 407 latexToMarkdown(e){ 408 var codeEditor = ace.edit("editorCode"); 409 var content = codeEditor.getCopyText(); 410 if (content === '') { 411 content = codeEditor.getValue(); 412 }
413 let slug = this.getSlug(); 414 axios.post('/advancedEdit/latexToMarkdown', { 415 'content': content, 416 'slug': slug, 417 } 418 ) 419 .then((response) => { 420 let markdown = response.data.markdown.trim(); 421 navigator.clipboard.writeText(markdown); 422 new Notify({ 423 status: 'success', 424 title: 'Code converted to markdown', 425 text: '<pre>' + markdown + '</pre>', 426 customIcon: '<i class="fa-regular fa-clipboard"></i>', 427 }) 428 }) 429 .catch(function (error) { 430 alert(error); 431 }) 432 } 433 openDialog(html) { 434 // let existing = document.getElementById('advancedEditDialog'); 435 // if (existing) existing.remove(); 436 // 437 // let dialog = document.createElement('dialog'); 438 // dialog.id = 'advancedEditDialog'; 439 // dialog.className = 'rounded border-0 shadow'; 440 // dialog.style.cssText = 'min-width: 50vw; max-width: 90vw; padding: 0;'; 441 // dialog.innerHTML = this.aiAnswer(html); 442 // document.body.appendChild(dialog); 443 // 444 // dialog.addEventListener('click', (e) => { 445 // if (e.target === dialog) dialog.close(); 446 // }); 447 let footer = document.getElementById('editorFooter'); 448 if (footer){ 449 footer.innerHTML = this.aiAnswer(html); 450 footer.style.display = 'block'; 451 footer.querySelectorAll('pre code').forEach((block) => hljs.highlightElement(block)); 452 document.getElementById('editorFooterClose').addEventListener('click', (e) => { 453 footer.style.display = 'none'; 454 }); 455 document.getElementById('advancedEditCopyBtn').addEventListener('click', () => { 456 let code = document.getElementById('corrected-text').innerText; 457 navigator.clipboard.writeText(code); 458 new Notify({ 459 status: 'success', 460 title: 'Code copied to clipboard', 461 text: '<pre>'+code+'</pre>', 462 customIcon: '<i class="fa-solid fa-copy"></i>', 463 autoClose: false, 464 }) 465 }); 466 } 467 468 469 // dialog.showModal(); 470 // 471 } 472 aiAnswer (html) 473 { 474 return ` 475 <div class="p-4"> 476 <div id="advancedEditDialogBody" style="max-height: 70vh; overflow-y: auto;">${html}</div> 477 <div class="d-flex justify-content-end gap-2 mt-3 pt-3 border-top"> 478 <button type="button" class="btn btn-primary" id="advancedEditCopyBtn"> 479 <i class="fa-solid fa-copy"></i> Copy 480 </button> 481 <form method="dialog" style="margin: 0;"> 482 <button type="submit" class="btn btn-secondary" id="editorFooterClose">Close</button> 483 </form> 484 </div> 485 </div>` 486 } 487 spelling(e) { 488 let codeEditor = ace.edit("editorCode"); 489 let content = codeEditor.getCopyText(); 490 if (content === ''){ 491 content = codeEditor.getValue(); 492 } 493 let slug = this.getSlug(); 494 axios.post('/aspell', { 495 'content': content, 496 'editor': "editorCode", 497 'slug': slug, 498 } 499 ) 500 .then((response) => { 501 let footer = document.getElementById('aspell-answer'); 502 if (footer){ 503 footer.style.display = 'block'; 504 footer.innerHTML = response.data.response; 505 } 506 }) 507 .catch(function (error) { 508 alert(error); 509 }) 510 } 511 syntax(e) { 512 let codeEditor = ace.edit("editorCode"); 513 let content = codeEditor.getCopyText(); 514 if (content === ''){ 515 content = codeEditor.getValue(); 516 } 517 let slug = this.getSlug(); 518 axios.post('/neuron/spelling', { 519 'content': content, 520 'slug': slug, 521 'name': 'Syntax' 522 } 523 ) 524 .then((response) => { 525 this.openDialog(response.data.response); 526 }) 527 .catch(function (error) { 528 alert(error); 529 }) 530 } 531 532 static getCookie(name) { 533 let cookies = document.cookie.split(';'); 534 let n = cookies.length; 535 for (let i = 0; i < n; i++) { 536 let cookie = cookies[i].split('='); 537 if (cookie[0].trim() === name) { 538 return cookie[1]; 539 } 540 } 541 return null; 542 } 543 static subTypes = [ 544 'col', 545 ] 546 getSlug(){ 547 let id = this.idValue; 548 let idArray = id.split('/'); 549 return idArray[0]; 550 } 551}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.