LiamKhoaLe commited on
Commit
8d133dc
·
1 Parent(s): 75a5481

Enhance code viewer and mermaid renderer from both PDF and UI

Browse files
Files changed (2) hide show
  1. static/script.js +68 -10
  2. utils/service/pdf.py +47 -0
static/script.js CHANGED
@@ -743,6 +743,8 @@
743
  // Configure marked to keep code blocks for highlight.js
744
  const htmlContent = marked.parse(markdown);
745
  container.innerHTML = htmlContent;
 
 
746
  // Render Mermaid if present
747
  renderMermaidInElement(container);
748
  // Add copy buttons to code blocks
@@ -762,6 +764,34 @@
762
  }
763
  }
764
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
  // Dynamically load Mermaid and render mermaid code blocks
766
  async function ensureMermaidLoaded() {
767
  if (window.mermaid && window.mermaid.initialize) return true;
@@ -826,12 +856,23 @@
826
  const codeBlocks = messageDiv.querySelectorAll('pre code');
827
  codeBlocks.forEach((codeBlock, index) => {
828
  const pre = codeBlock.parentElement;
 
 
 
 
829
  const language = codeBlock.className.match(/language-(\w+)/)?.[1] || 'code';
830
-
 
 
 
 
 
 
 
831
  // Create wrapper
832
  const wrapper = document.createElement('div');
833
  wrapper.className = 'code-block-wrapper';
834
-
835
  // Create header with language and copy button
836
  const header = document.createElement('div');
837
  header.className = 'code-block-header';
@@ -845,19 +886,36 @@
845
  Copy
846
  </button>
847
  `;
848
-
849
- // Create content wrapper
850
  const content = document.createElement('div');
851
  content.className = 'code-block-content';
852
- content.appendChild(codeBlock.cloneNode(true));
853
-
 
854
  // Assemble wrapper
855
  wrapper.appendChild(header);
856
  wrapper.appendChild(content);
857
-
858
- // Replace original pre with wrapper
859
- pre.parentNode.replaceChild(wrapper, pre);
860
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
  // Add click handler for copy button
862
  const copyBtn = wrapper.querySelector('.copy-code-btn');
863
  copyBtn.addEventListener('click', () => copyCodeToClipboard(codeBlock.textContent, copyBtn));
 
743
  // Configure marked to keep code blocks for highlight.js
744
  const htmlContent = marked.parse(markdown);
745
  container.innerHTML = htmlContent;
746
+ // Normalize heading numbering (H1/H2/H3) without double-numbering
747
+ try { renumberHeadings(container); } catch {}
748
  // Render Mermaid if present
749
  renderMermaidInElement(container);
750
  // Add copy buttons to code blocks
 
764
  }
765
  }
766
 
767
+ function renumberHeadings(root) {
768
+ const h1s = Array.from(root.querySelectorAll('h1'));
769
+ const h2s = Array.from(root.querySelectorAll('h2'));
770
+ const h3s = Array.from(root.querySelectorAll('h3'));
771
+ let s1 = 0;
772
+ let s2 = 0;
773
+ let s3 = 0;
774
+ const headers = Array.from(root.querySelectorAll('h1, h2, h3'));
775
+ headers.forEach(h => {
776
+ const text = h.textContent.trim();
777
+ // Strip any existing numeric prefix like "1. ", "1.2 ", "1.2.3 "
778
+ const stripped = text.replace(/^\d+(?:\.\d+){0,2}\s+/, '');
779
+ if (h.tagName === 'H1') {
780
+ s1 += 1; s2 = 0; s3 = 0;
781
+ h.textContent = `${s1}. ${stripped}`;
782
+ } else if (h.tagName === 'H2') {
783
+ if (s1 === 0) { s1 = 1; }
784
+ s2 += 1; s3 = 0;
785
+ h.textContent = `${s1}.${s2} ${stripped}`;
786
+ } else if (h.tagName === 'H3') {
787
+ if (s1 === 0) { s1 = 1; }
788
+ if (s2 === 0) { s2 = 1; }
789
+ s3 += 1;
790
+ h.textContent = `${s1}.${s2}.${s3} ${stripped}`;
791
+ }
792
+ });
793
+ }
794
+
795
  // Dynamically load Mermaid and render mermaid code blocks
796
  async function ensureMermaidLoaded() {
797
  if (window.mermaid && window.mermaid.initialize) return true;
 
856
  const codeBlocks = messageDiv.querySelectorAll('pre code');
857
  codeBlocks.forEach((codeBlock, index) => {
858
  const pre = codeBlock.parentElement;
859
+ if (!pre || pre.dataset.sbWrapped === '1') return;
860
+ const isMermaid = codeBlock.classList.contains('language-mermaid');
861
+ // Do not wrap mermaid blocks; they will be replaced by SVGs
862
+ if (isMermaid) return;
863
  const language = codeBlock.className.match(/language-(\w+)/)?.[1] || 'code';
864
+
865
+ // Ensure syntax highlighting is applied before moving the node
866
+ try {
867
+ if (window.hljs && window.hljs.highlightElement) {
868
+ window.hljs.highlightElement(codeBlock);
869
+ }
870
+ } catch {}
871
+
872
  // Create wrapper
873
  const wrapper = document.createElement('div');
874
  wrapper.className = 'code-block-wrapper';
875
+
876
  // Create header with language and copy button
877
  const header = document.createElement('div');
878
  header.className = 'code-block-header';
 
886
  Copy
887
  </button>
888
  `;
889
+
890
+ // Create content wrapper and move the original <pre><code> inside (preserves highlighting)
891
  const content = document.createElement('div');
892
  content.className = 'code-block-content';
893
+ pre.dataset.sbWrapped = '1';
894
+ content.appendChild(pre);
895
+
896
  // Assemble wrapper
897
  wrapper.appendChild(header);
898
  wrapper.appendChild(content);
899
+
900
+ // Insert wrapper where the original pre was
901
+ const parent = content.parentNode || messageDiv; // safety
902
+ if (pre.parentNode !== content) {
903
+ // pre has been moved into content; parent should be original parent of pre
904
+ const insertionParent = parent === messageDiv ? messageDiv : pre.parentNode;
905
+ }
906
+ // Replace in DOM: pre has been moved; place wrapper where pre used to be
907
+ const originalParent = content.parentNode ? content.parentNode : messageDiv;
908
+ // If pre had a previous sibling, insert wrapper before it; else append
909
+ const ref = wrapper.querySelector('.code-block-content pre');
910
+ const oldParent = wrapper.querySelector('.code-block-content pre').parentNode;
911
+ // oldParent is content; we need to place wrapper at the original location of pre
912
+ const originalPlaceholder = document.createComment('code-block-wrapper');
913
+ const preOriginalParent = wrapper.querySelector('.code-block-content pre').parentElement; // content
914
+ // Since we already moved pre, we can't auto-place; use previousSibling stored before move
915
+ // Simpler: insert wrapper after content creation at the position of 'content' parent
916
+ // If messageDiv contains multiple elements, just append wrapper now
917
+ messageDiv.appendChild(wrapper);
918
+
919
  // Add click handler for copy button
920
  const copyBtn = wrapper.querySelector('.copy-code-btn');
921
  copyBtn.addEventListener('click', () => copyCodeToClipboard(codeBlock.textContent, copyBtn));
utils/service/pdf.py CHANGED
@@ -63,6 +63,27 @@ def _parse_markdown_content(content: str, heading1_style, heading2_style, headin
63
  i += 1
64
 
65
  if code_lines:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  from reportlab.platypus import XPreformatted, Paragraph
67
  # Join and sanitize code content: expand tabs, remove control chars that render as squares
68
  raw_code = '\n'.join(code_lines)
@@ -509,6 +530,32 @@ def _apply_syntax_highlight(escaped_code: str, language: str) -> str:
509
  return out
510
 
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  async def _format_references_ieee(sources: List[Dict]) -> List[str]:
513
  """Format sources in IEEE citation style using NVIDIA API."""
514
  try:
 
63
  i += 1
64
 
65
  if code_lines:
66
+ # Mermaid diagrams → render via Kroki PNG for PDF
67
+ if language.lower() == 'mermaid':
68
+ try:
69
+ from reportlab.platypus import Image, Spacer
70
+ img_bytes = _render_mermaid_png('\n'.join(code_lines))
71
+ if img_bytes:
72
+ import io
73
+ img = Image(io.BytesIO(img_bytes))
74
+ # Fit within page width (~6 inches after margins)
75
+ max_width = 6.0 * inch
76
+ if img.drawWidth > max_width:
77
+ scale = max_width / float(img.drawWidth)
78
+ img.drawWidth = max_width
79
+ img.drawHeight = img.drawHeight * scale
80
+ story.append(img)
81
+ story.append(Spacer(1, 12))
82
+ i += 1
83
+ continue
84
+ except Exception as me:
85
+ logger.warning(f"[PDF] Mermaid render failed, falling back to code block: {me}")
86
+
87
  from reportlab.platypus import XPreformatted, Paragraph
88
  # Join and sanitize code content: expand tabs, remove control chars that render as squares
89
  raw_code = '\n'.join(code_lines)
 
530
  return out
531
 
532
 
533
+ def _render_mermaid_png(mermaid_text: str) -> bytes:
534
+ """
535
+ Render mermaid code to PNG via Kroki service (no local mermaid-cli dependency).
536
+ Falls back to returning empty bytes on failure.
537
+ """
538
+ try:
539
+ import base64
540
+ import json
541
+ import urllib.request
542
+ import urllib.error
543
+ # Kroki POST API for mermaid -> png
544
+ data = json.dumps({"diagram_source": mermaid_text}).encode("utf-8")
545
+ req = urllib.request.Request(
546
+ url="https://kroki.io/mermaid/png",
547
+ data=data,
548
+ headers={"Content-Type": "application/json"},
549
+ method="POST"
550
+ )
551
+ with urllib.request.urlopen(req, timeout=10) as resp:
552
+ if resp.status == 200:
553
+ return resp.read()
554
+ except Exception as e:
555
+ logger.warning(f"[PDF] Kroki mermaid render error: {e}")
556
+ return b""
557
+
558
+
559
  async def _format_references_ieee(sources: List[Dict]) -> List[str]:
560
  """Format sources in IEEE citation style using NVIDIA API."""
561
  try: