Skip to content

Commit b9166c0

Browse files
authored
allow custom static and media urls in html export (#407)
Co-authored-by: viseshrp <[email protected]>
1 parent b5911ac commit b9166c0

File tree

4 files changed

+303
-199
lines changed

4 files changed

+303
-199
lines changed

src/ansys/dynamicreporting/core/serverless/adr.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ def __init__(
138138
databases: dict | None = None,
139139
media_directory: str | None = None,
140140
static_directory: str | None = None,
141-
media_url: str | None = None,
142-
static_url: str | None = None,
141+
media_url: str = "/media/",
142+
static_url: str = "/static/",
143143
debug: bool | None = None,
144144
opts: dict | None = None,
145145
request: HttpRequest | None = None,
@@ -686,15 +686,11 @@ def static_directory(self) -> str:
686686

687687
@property
688688
def static_url(self) -> str:
689-
from django.conf import settings
690-
691-
return settings.STATIC_URL
689+
return self._static_url
692690

693691
@property
694692
def media_url(self) -> str:
695-
from django.conf import settings
696-
697-
return settings.MEDIA_URL
693+
return self._media_url
698694

699695
@property
700696
def session(self) -> Session:
@@ -1038,8 +1034,10 @@ def export_report_as_html(
10381034
exporter = ServerlessReportExporter(
10391035
html_content=html_content,
10401036
output_dir=output_dir,
1041-
static_dir=self._static_directory,
10421037
media_dir=self._media_directory,
1038+
static_dir=self._static_directory,
1039+
media_url=self._media_url,
1040+
static_url=self._static_url,
10431041
filename=filename,
10441042
ansys_version=str(self._ansys_version),
10451043
dark_mode=dark_mode,

src/ansys/dynamicreporting/core/serverless/html_exporter.py

Lines changed: 120 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import os
33
from pathlib import Path
4+
import re
45
from typing import Any
56

67
from ..adr_utils import get_logger
@@ -16,7 +17,6 @@
1617
VIEWER_JS,
1718
VIEWER_UTILS,
1819
)
19-
from ..utils.report_download_html import ReportDownloadHTML
2020

2121

2222
class ServerlessReportExporter:
@@ -34,8 +34,10 @@ def __init__(
3434
self,
3535
html_content: str,
3636
output_dir: Path,
37-
static_dir: Path,
3837
media_dir: Path,
38+
static_dir: Path,
39+
media_url: str,
40+
static_url: str,
3941
*,
4042
filename: str = "index.html",
4143
no_inline_files: bool = False,
@@ -49,16 +51,18 @@ def __init__(
4951
"""
5052
self._html_content = html_content
5153
self._output_dir = output_dir
52-
self._static_dir = static_dir
5354
self._media_dir = media_dir
55+
self._static_dir = static_dir
56+
self._media_url = media_url
57+
self._static_url = static_url
5458
self._filename = filename
5559
self._debug = debug
5660
self._logger = logger or get_logger()
5761
self._no_inline = no_inline_files
5862
self._ansys_version = ansys_version
5963
self._dark_mode = dark_mode
6064

61-
# State tracking properties, functionally identical to ReportDownloadHTML
65+
# State tracking properties
6266
self._filemap: dict[str, str] = {}
6367
self._replaced_file_ext: str | None = None
6468
self._collision_count = 0
@@ -96,7 +100,8 @@ def export(self) -> None:
96100
html = self._replace_blocks(html, "<link", "/>")
97101
html = self._replace_blocks(html, "<img id='guiicon", ">")
98102
html = self._replace_blocks(html, "e.src = '", "';")
99-
html = self._replace_blocks(html, "<script src=", "</script>")
103+
html = self._replace_blocks(html, "<script src=", "</script>") # src-first scripts
104+
html = self._replace_blocks(html, "<script", "</script>") # any-order scripts (MathJax)
100105
html = self._replace_blocks(html, "<a href=", ">")
101106
html = self._replace_blocks(html, "<img src=", ">")
102107
html = self._replace_blocks(html, "<source src=", ">")
@@ -161,19 +166,24 @@ def _replace_files(self, text: str, inline: bool = False, size_check: bool = Fal
161166
current = 0
162167
ver = str(self._ansys_version) if self._ansys_version is not None else ""
163168

164-
patterns = (
165-
f"/static/ansys{ver}/",
166-
"/static/",
167-
"/media/",
168-
f"/ansys{ver}/",
169-
)
169+
patterns = []
170+
if ver:
171+
patterns.append(f"{self._static_url}ansys{ver}/") # custom
172+
patterns.append(f"/static/ansys{ver}/") # legacy literal
173+
174+
# custom first, then legacy literals
175+
patterns.extend([self._static_url, self._media_url, "/static/", "/media/"])
176+
177+
if ver:
178+
patterns.append(f"/ansys{ver}/") # server-root style
170179

171180
while True:
172181
# Find the next match using the legacy priority order
173182
idx1 = -1
174183
for pat in patterns:
175-
idx1 = text.find(pat, current)
176-
if idx1 != -1:
184+
pos = text.find(pat, current)
185+
if pos != -1:
186+
idx1 = pos
177187
break
178188
if idx1 == -1:
179189
return text # nothing more to replace
@@ -192,7 +202,6 @@ def _replace_files(self, text: str, inline: bool = False, size_check: bool = Fal
192202
self._replaced_file_ext = ext
193203

194204
new_path = self._process_file(path_in_html, simple_path, inline=inline)
195-
196205
if size_check and self._inline_size_exception:
197206
new_path = "__SIZE_EXCEPTION__"
198207

@@ -309,9 +318,7 @@ def _copy_static_file(self, source_rel_path: str, target_rel_path: str):
309318
target_file.parent.mkdir(parents=True, exist_ok=True)
310319
content = source_file.read_bytes()
311320
# Patch some viewer JS internals (loader/paths) if needed
312-
content = ReportDownloadHTML.fix_viewer_component_paths(
313-
str(target_file), content, self._ansys_version
314-
)
321+
content = self._fix_viewer_component_paths(str(target_file), content)
315322
target_file.write_bytes(content)
316323
else:
317324
self._logger.warning(f"Warning: Static source file not found: {source_file}")
@@ -322,7 +329,7 @@ def _copy_static_files(self, files: list[str], source_prefix: str, target_prefix
322329
self._copy_static_file(source_prefix.lstrip("/") + f, target_prefix + f)
323330

324331
def _make_unique_basename(self, name: str) -> str:
325-
"""Ensures a unique filename in the target media directory to avoid collisions."""
332+
"""Ensures a unique filename in the target media directory to avoid collisions (legacy)."""
326333
if not self._no_inline:
327334
return name
328335
target_path = self._output_dir / "media" / name
@@ -331,6 +338,10 @@ def _make_unique_basename(self, name: str) -> str:
331338
self._collision_count += 1
332339
return f"{self._collision_count}_{name}"
333340

341+
@staticmethod
342+
def is_scene_file(name: str) -> bool:
343+
return name.upper().endswith((".AVZ", ".SCDOC", ".SCDOCX", ".GLB"))
344+
334345
def _process_file(self, path_in_html: str, pathname: str, inline: bool = False) -> str:
335346
"""
336347
Reads a file from local disk and either inlines it or copies it.
@@ -348,12 +359,18 @@ def _process_file(self, path_in_html: str, pathname: str, inline: bool = False)
348359
return self._filemap[pathname]
349360

350361
# Resolve source file location based on the raw pathname (no normalization)
351-
if pathname.startswith("/media/"):
362+
ver = str(self._ansys_version) if self._ansys_version is not None else ""
363+
364+
# Source resolution: custom first, then legacy literals
365+
if pathname.startswith(self._media_url):
366+
source_file = self._media_dir / pathname.replace(self._media_url, "", 1)
367+
elif pathname.startswith(self._static_url):
368+
source_file = self._static_dir / pathname.replace(self._static_url, "", 1)
369+
elif pathname.startswith("/media/"): # legacy literal
352370
source_file = self._media_dir / pathname.replace("/media/", "", 1)
353-
elif pathname.startswith("/static/"):
371+
elif pathname.startswith("/static/"): # legacy literal
354372
source_file = self._static_dir / pathname.replace("/static/", "", 1)
355-
elif pathname.startswith(f"/ansys{self._ansys_version}/"):
356-
# Legacy downloads these from the server root; serverless reads them from static dir
373+
elif ver and pathname.startswith(f"/ansys{ver}/"):
357374
source_file = self._static_dir / pathname.lstrip("/")
358375
else:
359376
source_file = None
@@ -375,7 +392,7 @@ def _process_file(self, path_in_html: str, pathname: str, inline: bool = False)
375392
# 4/3 is roughly the expansion factor of base64 encoding (3 bytes -> 4 chars)
376393
estimated_inline_size = int(len(content) * (4.0 / 3.0))
377394

378-
if (inline or ReportDownloadHTML.is_scene_file(pathname)) and self._should_use_data_uri(
395+
if (inline or self.is_scene_file(pathname)) and self._should_use_data_uri(
379396
estimated_inline_size
380397
):
381398
# Inline as data URI
@@ -398,68 +415,126 @@ def _process_file(self, path_in_html: str, pathname: str, inline: bool = False)
398415
# prefix with parent folder (GUID) like legacy
399416
basename = f"{source_file.parent.name}_{basename}"
400417
else:
401-
content = ReportDownloadHTML.fix_viewer_component_paths(
402-
basename, content, self._ansys_version
403-
)
418+
content = self._fix_viewer_component_paths(basename, content)
404419

405-
# Output path (exact legacy behavior):
406-
# - If /static/ansys{ver}/ -> keep ansys tree, remove '/static/' -> './ansys{ver}/.../<basename>'
407-
# - Else -> './media/<basename>'
408-
if pathname.startswith(f"/static/ansys{self._ansys_version}/"):
409-
local_pathname = os.path.dirname(pathname).replace("/static/", "./", 1)
410-
result = f"{local_pathname}/{basename}"
420+
# Output path:
421+
# keep ansys tree if the input path came from /static/ansys{ver}/ (custom OR legacy literal)
422+
if ver and (
423+
pathname.startswith(f"{self._static_url}ansys{ver}/")
424+
or pathname.startswith(f"/static/ansys{ver}/")
425+
):
426+
local_pathname = os.path.dirname(pathname)
427+
# normalize either custom or legacy /static/ to './'
428+
if local_pathname.startswith(self._static_url):
429+
local_pathname = local_pathname.replace(self._static_url, "./", 1)
430+
elif local_pathname.startswith("/static/"):
431+
local_pathname = local_pathname.replace("/static/", "./", 1)
411432

433+
result = f"{local_pathname}/{basename}"
412434
target_file = self._output_dir / local_pathname.lstrip("./") / basename
413-
target_file.parent.mkdir(parents=True, exist_ok=True)
414-
target_file.write_bytes(content)
415435
else:
416436
result = f"./media/{basename}"
417-
418437
target_file = self._output_dir / "media" / basename
419-
target_file.parent.mkdir(parents=True, exist_ok=True)
420-
target_file.write_bytes(content)
438+
439+
target_file.parent.mkdir(parents=True, exist_ok=True)
440+
target_file.write_bytes(content)
421441

422442
self._filemap[pathname] = result
423443
return result
424444

445+
def _find_block(self, text: str, start: int, prefix: str, suffix: str) -> tuple[int, int, str]:
446+
"""
447+
Legacy-compatible: return the next [prefix ... suffix] block that contains at least
448+
one asset-like reference. Accept both the literal legacy prefixes and the configured
449+
custom prefixes. Also accept any '/ansys<ver>/' or generic '/ansys<digits>/'.
450+
"""
451+
# Normalize known prefixes (custom URLs may differ from /static/ and /media/)
452+
custom_static = (self._static_url or "").strip()
453+
custom_media = (self._media_url or "").strip()
454+
455+
while True:
456+
try:
457+
idx1 = text.index(prefix, start)
458+
except ValueError:
459+
return -1, -1, ""
460+
try:
461+
idx2 = text.index(suffix, idx1 + len(prefix))
462+
except ValueError:
463+
return -1, -1, ""
464+
idx2 += len(suffix)
465+
block = text[idx1:idx2]
466+
467+
if (
468+
("/static/" in block)
469+
or ("/media/" in block)
470+
or (custom_static and custom_static in block)
471+
or (custom_media and custom_media in block)
472+
or re.search(r"/ansys\d+/", block) is not None
473+
):
474+
return idx1, idx2, block
475+
476+
start = idx2
477+
425478
def _replace_blocks(
426479
self, html: str, prefix: str, suffix: str, inline: bool = False, size_check: bool = False
427480
) -> str:
428481
"""Iteratively finds and replaces all asset references within matching blocks."""
429482
current_pos = 0
430483
while True:
431-
start, end, text_block = ReportDownloadHTML.find_block(
432-
html, current_pos, prefix, suffix
433-
)
484+
start, end, text_block = self._find_block(html, current_pos, prefix, suffix)
434485
if start < 0:
435486
break
436487
processed_text = self._replace_files(text_block, inline=inline, size_check=size_check)
437488
html = html[:start] + processed_text + html[end:]
438489
current_pos = start + len(processed_text)
439490
return html
440491

492+
def _fix_viewer_component_paths(self, filename: str, data: bytes) -> bytes:
493+
"""
494+
Adjust hard-coded viewer paths for offline export, honoring custom self._static_url.
495+
Mirrors legacy behavior but replaces the '/static/' prefix with self._static_url.
496+
"""
497+
ver = str(self._ansys_version) if self._ansys_version is not None else ""
498+
if filename.endswith("ANSYSViewer_min.js"):
499+
s = data.decode("utf-8")
500+
# Replace "<static>/website/images/" with dynamic base to ./media/
501+
s = s.replace(
502+
f'"{self._static_url}website/images/"',
503+
r'document.URL.replace(/\\/g, "/").replace("index.html", "media/")',
504+
)
505+
# Point ansys images to local ./ansys{ver}//nexus/images/
506+
if ver:
507+
s = s.replace(f'"/ansys{ver}/nexus/images/', f'"./ansys{ver}//nexus/images/')
508+
# Allow file:// loads in offline mode (legacy behavior)
509+
s = s.replace('"FILE",delegate', '"arraybuffer",delegate')
510+
return s.encode("utf-8")
511+
512+
if filename.endswith("viewer-loader.js"):
513+
s = data.decode("utf-8")
514+
if ver:
515+
s = s.replace(f'"/ansys{ver}/nexus/images/', f'"./ansys{ver}//nexus/images/')
516+
return s.encode("utf-8")
517+
518+
return data
519+
441520
def _inline_ansys_viewer(self, html: str) -> str:
442521
"""Handles the special case of inlining assets for the <ansys-nexus-viewer> component."""
443522
current_pos = 0
444523
while True:
445-
start, end, text_block = ReportDownloadHTML.find_block(
524+
start, end, text_block = self._find_block(
446525
html, current_pos, "<ansys-nexus-viewer", "</ansys-nexus-viewer>"
447526
)
448527
if start < 0:
449528
break
450-
451529
# Legacy parity: always inline viewer attributes
452530
text = self._replace_blocks(text_block, 'proxy_img="', '"', inline=True)
453531
text = self._replace_blocks(text, 'src="', '"', inline=True, size_check=True)
454-
455532
if "__SIZE_EXCEPTION__" in text:
456533
msg = "3D geometry too large for stand-alone HTML file"
457534
text = text.replace('src="__SIZE_EXCEPTION__"', f'src="" proxy_only="{msg}"')
458-
459535
if self._replaced_file_ext:
460536
ext = self._replaced_file_ext.replace(".", "").upper()
461537
text = text.replace("<ansys-nexus-viewer", f'<ansys-nexus-viewer src_ext="{ext}"')
462-
463538
html = html[:start] + text + html[end:]
464539
current_pos = start + len(text)
465540
return html

0 commit comments

Comments
 (0)