euler314 commited on
Commit
d0f80bb
·
verified ·
1 Parent(s): f282d19

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +206 -157
app.py CHANGED
@@ -9,6 +9,9 @@ import io
9
  from PIL import Image
10
  import fitz # PyMuPDF
11
  import re
 
 
 
12
 
13
  # Set page configuration
14
  st.set_page_config(page_title="LaTeX Editor & Compiler", page_icon="📝", layout="wide")
@@ -86,36 +89,6 @@ def render_pdf_preview(pdf_data):
86
  st.error(f"Error rendering PDF preview: {str(e)}")
87
  return None
88
 
89
- # Extract partial command from cursor position
90
- def get_current_command(text, cursor_pos):
91
- if cursor_pos <= 0:
92
- return ""
93
-
94
- # Look for the last backslash before cursor
95
- start_pos = text[:cursor_pos].rfind("\\")
96
- if start_pos == -1:
97
- return ""
98
-
99
- # Extract the partial command (from \ to cursor)
100
- partial_cmd = text[start_pos:cursor_pos]
101
- return partial_cmd
102
-
103
- # Find matching commands for autocomplete
104
- def find_matching_commands(partial_cmd, all_commands):
105
- if not partial_cmd.startswith("\\"):
106
- return []
107
-
108
- partial_without_backslash = partial_cmd[1:].lower()
109
- matches = []
110
-
111
- for cmd in all_commands:
112
- cmd_without_backslash = cmd[1:].lower()
113
- # Check if command contains the partial input (without \)
114
- if cmd_without_backslash.startswith(partial_without_backslash):
115
- matches.append(cmd)
116
-
117
- return matches
118
-
119
  # LaTeX package reference
120
  latex_packages = {
121
  "Document": {
@@ -225,10 +198,11 @@ latex_commands = {
225
  }
226
  }
227
 
228
- # Create a flat list of all LaTeX commands for autocomplete
229
- all_latex_commands = []
230
  for category, commands in latex_commands.items():
231
- all_latex_commands.extend(list(commands.keys()))
 
232
 
233
  # Default LaTeX template
234
  default_template = r"""\documentclass{article}
@@ -302,7 +276,141 @@ Your conclusion here.
302
  \end{document}
303
  """
304
 
305
- # Add custom CSS with improved sidebar styling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  st.markdown("""
307
  <style>
308
  /* Editor styling */
@@ -312,11 +420,6 @@ st.markdown("""
312
  padding: 10px;
313
  background-color: #f8f9fa;
314
  }
315
- .stTextArea textarea {
316
- font-family: 'Courier New', Courier, monospace !important;
317
- font-size: 14px !important;
318
- line-height: 1.5 !important;
319
- }
320
 
321
  /* Download button styling */
322
  .download-button {
@@ -414,62 +517,44 @@ st.markdown("""
414
  background-color: #f8f9fa;
415
  }
416
 
417
- /* Autocomplete suggestions */
418
- .autocomplete-container {
419
- background-color: #f8f9fa;
420
- border: 1px solid #dee2e6;
421
- border-radius: 4px;
422
- padding: 10px;
423
- margin-top: 10px;
424
- max-height: 200px;
425
- overflow-y: auto;
426
- }
427
 
428
- .suggestion-item {
429
- padding: 5px;
430
- cursor: pointer;
431
- border-radius: 3px;
432
- }
433
 
434
- .suggestion-item:hover {
435
- background-color: #e9ecef;
436
- }
437
 
438
- /* Preview toggle button */
439
- .preview-button {
440
- margin-top: 15px;
441
- width: 100%;
442
- }
443
- </style>
444
- """, unsafe_allow_html=True)
445
 
446
  # Main application
447
  def main():
448
  st.title("LaTeX Editor & PDF Compiler")
449
 
450
- # Initialize session state variables
451
- if 'latex_code' not in st.session_state:
452
- st.session_state.latex_code = default_template
453
- if 'cursor_pos' not in st.session_state:
454
- st.session_state.cursor_pos = 0
455
- if 'show_preview' not in st.session_state:
456
- st.session_state.show_preview = False
457
-
458
  # Display installation status
459
  if not is_pdflatex_installed():
460
  st.warning("⚠️ LaTeX is not installed correctly. The PDF compilation feature will not work.")
461
  st.info("For Hugging Face Spaces, make sure you have a packages.txt file with the necessary LaTeX packages.")
462
-
463
- # Show packages.txt content suggestion
464
- with st.expander("Required packages.txt content"):
465
- st.code("""texlive
466
- texlive-latex-base
467
- texlive-latex-extra
468
- texlive-fonts-recommended
469
- texlive-science
470
- python3-dev
471
- python3-pip
472
- poppler-utils""", language="text")
473
 
474
  # Create layout
475
  col1, col2 = st.columns([3, 2])
@@ -477,50 +562,12 @@ poppler-utils""", language="text")
477
  with col1:
478
  st.subheader("LaTeX Editor")
479
 
480
- # Detect current command for autocomplete
481
- if 'last_input' in st.session_state:
482
- current_command = get_current_command(st.session_state.latex_code, st.session_state.cursor_pos)
483
- matching_commands = find_matching_commands(current_command, all_latex_commands)
484
-
485
- # Display autocomplete suggestions when typing a command
486
- if current_command and matching_commands:
487
- st.markdown("### Command Suggestions")
488
- st.markdown('<div class="autocomplete-container">', unsafe_allow_html=True)
489
- for cmd in matching_commands[:10]: # Limit to 10 suggestions
490
- if st.button(cmd, key=f"suggest_{cmd}"):
491
- # Replace partial command with the full command
492
- text_before = st.session_state.latex_code[:st.session_state.cursor_pos - len(current_command)]
493
- text_after = st.session_state.latex_code[st.session_state.cursor_pos:]
494
- st.session_state.latex_code = text_before + cmd + text_after
495
- st.rerun()
496
- st.markdown('</div>', unsafe_allow_html=True)
497
-
498
- # LaTeX editor
499
- latex_code = st.text_area(
500
- "Edit your LaTeX document:",
501
- value=st.session_state.latex_code,
502
- height=500,
503
- key="latex_editor",
504
- on_change=lambda: setattr(st.session_state, 'last_input', True)
505
- )
506
  st.session_state.latex_code = latex_code
507
 
508
- # Get cursor position for autocomplete (using JS - note this is limited in Streamlit)
509
- # This is a workaround since Streamlit doesn't directly expose cursor position
510
- st.markdown("""
511
- <script>
512
- document.addEventListener('DOMContentLoaded', function() {
513
- const textArea = document.querySelector('textarea');
514
- if (textArea) {
515
- textArea.addEventListener('keyup', function(e) {
516
- const cursorPos = this.selectionStart;
517
- // Would need a way to communicate this to Python
518
- console.log("Cursor position:", cursorPos);
519
- });
520
- }
521
- });
522
- </script>
523
- """, unsafe_allow_html=True)
524
 
525
  # Control buttons
526
  col1_1, col1_2, col1_3 = st.columns(3)
@@ -598,8 +645,8 @@ poppler-utils""", language="text")
598
  # LaTeX Reference Sidebar
599
  st.sidebar.title("LaTeX Reference")
600
 
601
- # Command quick search
602
- quick_search = st.sidebar.text_input("Type to find commands (e.g. 'fr' for '\\frac')")
603
 
604
  if quick_search:
605
  # Find and display matching commands
@@ -618,42 +665,44 @@ poppler-utils""", language="text")
618
  st.markdown(f"<div><span class='latex-command'>{cmd}</span> <small>{category}</small></div>", unsafe_allow_html=True)
619
  with col2:
620
  if st.button("Insert", key=f"quick_{cmd}"):
 
621
  st.session_state.latex_code += f"\n{cmd}"
622
  st.rerun()
623
  else:
624
  st.sidebar.info("No matching commands found")
625
 
626
  # Regular categories
627
- if not quick_search:
628
- tab1, tab2 = st.sidebar.tabs(["Commands", "Packages"])
629
-
630
- with tab1:
631
- for category, commands in latex_commands.items():
632
- with st.expander(category, expanded=category=="Math"):
633
- st.markdown('<div class="command-list">', unsafe_allow_html=True)
634
- for cmd, desc in commands.items():
635
- col1, col2 = st.sidebar.columns([4, 1])
636
- with col1:
637
- st.markdown(f"<div><span class='latex-command'>{cmd}</span></div>", unsafe_allow_html=True)
638
- with col2:
639
- if st.button("Insert", key=f"btn_{cmd}"):
640
- st.session_state.latex_code += f"\n{cmd}"
641
- st.rerun()
642
- st.markdown('</div>', unsafe_allow_html=True)
643
-
644
- with tab2:
645
- for category, packages in latex_packages.items():
646
- with st.expander(category):
647
- st.markdown('<div class="command-list">', unsafe_allow_html=True)
648
- for pkg, desc in packages.items():
649
- col1, col2 = st.sidebar.columns([4, 1])
650
- with col1:
651
- st.markdown(f"<div><span class='latex-command'>{pkg}</span></div>", unsafe_allow_html=True)
652
- with col2:
653
- if st.button("Insert", key=f"btn_{pkg}"):
654
- st.session_state.latex_code += f"\n{pkg}"
655
- st.rerun()
656
- st.markdown('</div>', unsafe_allow_html=True)
 
657
 
658
  if __name__ == "__main__":
659
  main()
 
9
  from PIL import Image
10
  import fitz # PyMuPDF
11
  import re
12
+ import json
13
+ import time
14
+ import components.html as html
15
 
16
  # Set page configuration
17
  st.set_page_config(page_title="LaTeX Editor & Compiler", page_icon="📝", layout="wide")
 
89
  st.error(f"Error rendering PDF preview: {str(e)}")
90
  return None
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  # LaTeX package reference
93
  latex_packages = {
94
  "Document": {
 
198
  }
199
  }
200
 
201
+ # Flatten all commands for autocomplete
202
+ all_commands = []
203
  for category, commands in latex_commands.items():
204
+ for cmd in commands:
205
+ all_commands.append(cmd)
206
 
207
  # Default LaTeX template
208
  default_template = r"""\documentclass{article}
 
276
  \end{document}
277
  """
278
 
279
+ # Create the Monaco Editor component with autocomplete
280
+ def create_monaco_editor():
281
+ # Convert the LaTeX commands to a format usable by Monaco
282
+ monaco_suggestions = []
283
+ for category, commands in latex_commands.items():
284
+ for cmd, desc in commands.items():
285
+ # Format command for Monaco
286
+ cmd_text = cmd.replace("{...}", "").replace("...", "")
287
+ monaco_suggestions.append({
288
+ "label": cmd_text,
289
+ "kind": 14, # Snippet
290
+ "insertText": cmd_text,
291
+ "detail": desc,
292
+ "documentation": f"Category: {category}"
293
+ })
294
+
295
+ # JSON stringify the suggestions for passing to JavaScript
296
+ suggestions_json = json.dumps(monaco_suggestions)
297
+
298
+ # Create the Monaco editor component with LaTeX syntax highlighting and autocomplete
299
+ monaco_editor = f"""
300
+ <div id="container" style="width:100%;height:500px;border:1px solid #ccc;"></div>
301
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.36.1/min/vs/loader.min.js"></script>
302
+ <script>
303
+ require.config({{ paths: {{ 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.36.1/min/vs' }}});
304
+
305
+ // Before loading vs/editor/editor.main, define a global MonacoEnvironment that overwrites
306
+ // the default worker url location (used when creating WebWorkers). The problem here is that
307
+ // HTML5 does not allow cross-domain web workers, so we need to proxy the instantiation of
308
+ // a web worker through a same-domain script
309
+ window.MonacoEnvironment = {{
310
+ getWorkerUrl: function(workerId, label) {{
311
+ return `data:text/javascript;charset=utf-8,
312
+ self.MonacoEnvironment = {{
313
+ baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.36.1/min/'
314
+ }};
315
+ importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.36.1/min/vs/base/worker/workerMain.js');`;
316
+ }}
317
+ }};
318
+
319
+ require(['vs/editor/editor.main'], function() {{
320
+ // Define LaTeX language
321
+ monaco.languages.register({{ id: 'latex' }});
322
+
323
+ // Define LaTeX syntax highlighting
324
+ monaco.languages.setMonarchTokensProvider('latex', {{
325
+ tokenizer: {{
326
+ root: [
327
+ [/\\\\[a-zA-Z]+/, 'keyword'],
328
+ [/\\\\begin\\{{[^}}]*\\}}/, 'keyword'],
329
+ [/\\\\end\\{{[^}}]*\\}}/, 'keyword'],
330
+ [/\\$\\$.*?\\$\\$/, 'string'],
331
+ [/\\$.*?\\$/, 'string'],
332
+ [/%.*$/, 'comment'],
333
+ [/\\{{/, 'delimiter.curly'],
334
+ [/\\}}/, 'delimiter.curly'],
335
+ [/\\[/, 'delimiter.square'],
336
+ [/\\]/, 'delimiter.square']
337
+ ]
338
+ }}
339
+ }});
340
+
341
+ // Setup autocomplete provider for LaTeX
342
+ monaco.languages.registerCompletionItemProvider('latex', {{
343
+ provideCompletionItems: function(model, position) {{
344
+ const textUntilPosition = model.getValueInRange({{
345
+ startLineNumber: position.lineNumber,
346
+ startColumn: 1,
347
+ endLineNumber: position.lineNumber,
348
+ endColumn: position.column
349
+ }});
350
+
351
+ // Check if we're typing a LaTeX command
352
+ const match = textUntilPosition.match(/\\\\([a-zA-Z]*)$/);
353
+ if (!match) return {{ suggestions: [] }};
354
+
355
+ const word = match[1];
356
+ const suggestions = {suggestions_json};
357
+
358
+ // Filter suggestions based on what has been typed
359
+ return {{
360
+ suggestions: suggestions.filter(function(s) {{
361
+ return s.label.substring(1).toLowerCase().indexOf(word.toLowerCase()) === 0;
362
+ }})
363
+ }};
364
+ }},
365
+ triggerCharacters: ['\\\\']
366
+ }});
367
+
368
+ // Get the initial value from localStorage or use default
369
+ let initialValue = localStorage.getItem('latexEditorContent') ||
370
+ {json.dumps(default_template)};
371
+
372
+ // Create the editor
373
+ const editor = monaco.editor.create(document.getElementById('container'), {{
374
+ value: initialValue,
375
+ language: 'latex',
376
+ theme: 'vs',
377
+ automaticLayout: true,
378
+ minimap: {{ enabled: false }},
379
+ fontSize: 14,
380
+ fontFamily: "'Courier New', monospace",
381
+ lineNumbers: "on",
382
+ scrollBeyondLastLine: false,
383
+ tabSize: 2,
384
+ wordWrap: "on"
385
+ }});
386
+
387
+ // Set up change handler to save content to localStorage
388
+ editor.onDidChangeModelContent(function() {{
389
+ const value = editor.getValue();
390
+ localStorage.setItem('latexEditorContent', value);
391
+
392
+ // Send value back to Streamlit
393
+ if (window.Streamlit) {{
394
+ window.Streamlit.setComponentValue(value);
395
+ }}
396
+ }});
397
+
398
+ // Handle initialization from Streamlit
399
+ window.addEventListener('message', function(event) {{
400
+ if (event.data.type === 'streamlit:render') {{
401
+ const args = event.data.args;
402
+ if (args.value) {{
403
+ editor.setValue(args.value);
404
+ }}
405
+ }}
406
+ }});
407
+ }});
408
+ </script>
409
+ """
410
+
411
+ return monaco_editor
412
+
413
+ # Add custom CSS
414
  st.markdown("""
415
  <style>
416
  /* Editor styling */
 
420
  padding: 10px;
421
  background-color: #f8f9fa;
422
  }
 
 
 
 
 
423
 
424
  /* Download button styling */
425
  .download-button {
 
517
  background-color: #f8f9fa;
518
  }
519
 
520
+ /* Hide Streamlit footer */
521
+ footer {display: none !important;}
522
+ #MainMenu {visibility: hidden;}
523
+ </style>
524
+ """, unsafe_allow_html=True)
525
+
526
+ # Create a component for the Monaco editor
527
+ def monaco_editor_component(key, default_value=""):
528
+ if key not in st.session_state:
529
+ st.session_state[key] = default_value
530
 
531
+ # Create a placeholder for the editor
532
+ placeholder = st.empty()
 
 
 
533
 
534
+ # Display the Monaco editor
535
+ editor_html = create_monaco_editor()
536
+ component_value = placeholder.html(editor_html, height=520)
537
 
538
+ # Return the current value
539
+ if component_value is not None:
540
+ st.session_state[key] = component_value
541
+
542
+ return st.session_state[key]
 
 
543
 
544
  # Main application
545
  def main():
546
  st.title("LaTeX Editor & PDF Compiler")
547
 
 
 
 
 
 
 
 
 
548
  # Display installation status
549
  if not is_pdflatex_installed():
550
  st.warning("⚠️ LaTeX is not installed correctly. The PDF compilation feature will not work.")
551
  st.info("For Hugging Face Spaces, make sure you have a packages.txt file with the necessary LaTeX packages.")
552
+
553
+ # Initialize session state
554
+ if 'latex_code' not in st.session_state:
555
+ st.session_state.latex_code = default_template
556
+ if 'show_preview' not in st.session_state:
557
+ st.session_state.show_preview = False
 
 
 
 
 
558
 
559
  # Create layout
560
  col1, col2 = st.columns([3, 2])
 
562
  with col1:
563
  st.subheader("LaTeX Editor")
564
 
565
+ # Use the Monaco editor component
566
+ latex_code = monaco_editor_component("monaco_editor", st.session_state.latex_code)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  st.session_state.latex_code = latex_code
568
 
569
+ # Create a small spacer
570
+ st.write("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
  # Control buttons
573
  col1_1, col1_2, col1_3 = st.columns(3)
 
645
  # LaTeX Reference Sidebar
646
  st.sidebar.title("LaTeX Reference")
647
 
648
+ # Command search
649
+ quick_search = st.sidebar.text_input("Find LaTeX Commands", "")
650
 
651
  if quick_search:
652
  # Find and display matching commands
 
665
  st.markdown(f"<div><span class='latex-command'>{cmd}</span> <small>{category}</small></div>", unsafe_allow_html=True)
666
  with col2:
667
  if st.button("Insert", key=f"quick_{cmd}"):
668
+ # Update LaTeX code with the inserted command
669
  st.session_state.latex_code += f"\n{cmd}"
670
  st.rerun()
671
  else:
672
  st.sidebar.info("No matching commands found")
673
 
674
  # Regular categories
675
+ tab1, tab2 = st.sidebar.tabs(["Commands", "Packages"])
676
+
677
+ with tab1:
678
+ for category, commands in latex_commands.items():
679
+ with st.expander(category, expanded=category=="Math"):
680
+ st.markdown('<div class="command-list">', unsafe_allow_html=True)
681
+ for cmd, desc in commands.items():
682
+ col1, col2 = st.sidebar.columns([4, 1])
683
+ with col1:
684
+ st.markdown(f"<div><span class='latex-command'>{cmd}</span></div>", unsafe_allow_html=True)
685
+ with col2:
686
+ if st.button("Insert", key=f"btn_{cmd}"):
687
+ # Update LaTeX code with the inserted command
688
+ st.session_state.latex_code += f"\n{cmd}"
689
+ st.rerun()
690
+ st.markdown('</div>', unsafe_allow_html=True)
691
+
692
+ with tab2:
693
+ for category, packages in latex_packages.items():
694
+ with st.expander(category):
695
+ st.markdown('<div class="command-list">', unsafe_allow_html=True)
696
+ for pkg, desc in packages.items():
697
+ col1, col2 = st.sidebar.columns([4, 1])
698
+ with col1:
699
+ st.markdown(f"<div><span class='latex-command'>{pkg}</span></div>", unsafe_allow_html=True)
700
+ with col2:
701
+ if st.button("Insert", key=f"btn_{pkg}"):
702
+ # Update LaTeX code with the inserted command
703
+ st.session_state.latex_code += f"\n{pkg}"
704
+ st.rerun()
705
+ st.markdown('</div>', unsafe_allow_html=True)
706
 
707
  if __name__ == "__main__":
708
  main()