deepresearch-flow 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. deepresearch_flow/paper/db.py +34 -0
  2. deepresearch_flow/paper/web/app.py +106 -1
  3. deepresearch_flow/paper/web/constants.py +5 -4
  4. deepresearch_flow/paper/web/handlers/__init__.py +2 -1
  5. deepresearch_flow/paper/web/handlers/api.py +55 -0
  6. deepresearch_flow/paper/web/handlers/pages.py +105 -25
  7. deepresearch_flow/paper/web/markdown.py +60 -0
  8. deepresearch_flow/paper/web/pdfjs/web/viewer.html +57 -5
  9. deepresearch_flow/paper/web/pdfjs/web/viewer.js +5 -1
  10. deepresearch_flow/paper/web/static/js/detail.js +494 -125
  11. deepresearch_flow/paper/web/static/js/outline.js +48 -34
  12. deepresearch_flow/paper/web/static_assets.py +289 -0
  13. deepresearch_flow/paper/web/templates/detail.html +46 -69
  14. deepresearch_flow/paper/web/templates/index.html +3 -3
  15. deepresearch_flow/paper/web/templates.py +7 -4
  16. deepresearch_flow/recognize/cli.py +805 -26
  17. deepresearch_flow/recognize/katex_check.js +29 -0
  18. deepresearch_flow/recognize/math.py +719 -0
  19. deepresearch_flow/recognize/mermaid.py +690 -0
  20. {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/METADATA +117 -4
  21. {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/RECORD +25 -21
  22. {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/WHEEL +0 -0
  23. {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/entry_points.txt +0 -0
  24. {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/licenses/LICENSE +0 -0
  25. {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,58 +1,72 @@
1
1
  /* Outline functionality extracted from outline_assets */
2
2
  (function() {
3
- function initOutline() {
4
- const content = document.getElementById('content');
5
- if (!content) return;
3
+ var bound = false;
6
4
 
7
- const headings = content.querySelectorAll('h2, h3, h4');
8
- if (headings.length === 0) return;
5
+ function buildOutline() {
6
+ var content = document.getElementById('content');
7
+ if (!content) return;
9
8
 
10
- const outline = document.getElementById('outline');
11
- const toggle = document.getElementById('outlineToggle');
12
- const close = document.getElementById('outlineClose');
13
- const outlineContent = document.getElementById('outlineContent');
9
+ var headings = content.querySelectorAll('h2, h3, h4');
10
+ var outline = document.getElementById('outline');
11
+ var toggle = document.getElementById('outlineToggle');
12
+ var close = document.getElementById('outlineClose');
13
+ var outlineContent = document.getElementById('outlineContent');
14
14
 
15
15
  if (!outline || !toggle || !close || !outlineContent) return;
16
16
 
17
- for (let i = 0; i < headings.length; i++) {
18
- const h = headings[i];
17
+ outlineContent.innerHTML = '';
18
+ if (headings.length === 0) return;
19
+ for (var i = 0; i < headings.length; i++) {
20
+ var h = headings[i];
19
21
  if (!h.id) h.id = 'heading-' + i;
20
- const a = document.createElement('a');
22
+ var a = document.createElement('a');
21
23
  a.href = '#' + h.id;
22
24
  a.textContent = h.textContent.trim();
23
25
  a.className = 'outline-' + h.tagName.toLowerCase();
24
26
  outlineContent.appendChild(a);
25
27
  }
26
28
 
27
- toggle.addEventListener('click', function() {
28
- outline.style.display = 'block';
29
- toggle.style.display = 'none';
30
- });
31
-
32
- close.addEventListener('click', function() {
33
- outline.style.display = 'none';
34
- toggle.style.display = 'block';
35
- });
36
-
37
- const savedState = sessionStorage.getItem('outlineVisible');
38
- if (savedState === 'true') {
39
- outline.style.display = 'block';
40
- toggle.style.display = 'none';
41
- } else {
42
- toggle.style.display = 'block';
29
+ if (!bound) {
30
+ toggle.addEventListener('click', function() {
31
+ outline.style.display = 'block';
32
+ toggle.style.display = 'none';
33
+ document.body.classList.add('outline-open');
34
+ });
35
+
36
+ close.addEventListener('click', function() {
37
+ outline.style.display = 'none';
38
+ toggle.style.display = 'block';
39
+ document.body.classList.remove('outline-open');
40
+ });
41
+
42
+ var savedState = sessionStorage.getItem('outlineVisible');
43
+ if (savedState === 'true') {
44
+ outline.style.display = 'block';
45
+ toggle.style.display = 'none';
46
+ document.body.classList.add('outline-open');
47
+ } else {
48
+ toggle.style.display = 'block';
49
+ document.body.classList.remove('outline-open');
50
+ }
51
+
52
+ var observer = new MutationObserver(function() {
53
+ var isVisible = outline.style.display === 'block';
54
+ sessionStorage.setItem('outlineVisible', String(isVisible));
55
+ });
56
+ observer.observe(outline, { attributes: true, attributeFilter: ['style'] });
57
+ bound = true;
43
58
  }
59
+ }
44
60
 
45
- const observer = new MutationObserver(function() {
46
- const isVisible = outline.style.display === 'block';
47
- sessionStorage.setItem('outlineVisible', String(isVisible));
48
- });
49
- observer.observe(outline, { attributes: true, attributeFilter: ['style'] });
61
+ function initOutline() {
62
+ buildOutline();
50
63
  }
51
64
 
52
- // Run when DOM is ready
53
65
  if (document.readyState === 'loading') {
54
66
  document.addEventListener('DOMContentLoaded', initOutline);
55
67
  } else {
56
68
  initOutline();
57
69
  }
70
+
71
+ document.addEventListener('content:updated', initOutline);
58
72
  })();
@@ -0,0 +1,289 @@
1
+ """Static asset export and URL mapping for paper web UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import base64
7
+ import hashlib
8
+ import mimetypes
9
+ import re
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from deepresearch_flow.paper.db_ops import PaperIndex
14
+
15
+ _IMAGE_PATTERN = re.compile(r"!\[([^\]]*)\]\(([^)]+)\)")
16
+ _DATA_URL_PATTERN = re.compile(r"^data:([^;,]+)(;base64)?,(.*)$", re.DOTALL)
17
+ _IMG_TAG_PATTERN = re.compile(r"<img\b[^>]*>", re.IGNORECASE)
18
+ _SRC_ATTR_PATTERN = re.compile(r"\bsrc\s*=\s*(\"[^\"]*\"|'[^']*'|[^\s>]+)", re.IGNORECASE | re.DOTALL)
19
+
20
+ _EXTENSION_OVERRIDES = {
21
+ ".jpe": ".jpg",
22
+ }
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class StaticAssetConfig:
27
+ enabled: bool
28
+ base_url: str | None
29
+ images_base_url: str | None
30
+ pdf_urls: dict[str, str]
31
+ md_urls: dict[str, str]
32
+ translated_md_urls: dict[str, dict[str, str]]
33
+
34
+
35
+ def _normalize_base_url(value: str) -> str:
36
+ return value.rstrip("/")
37
+
38
+
39
+ def _extension_from_mime(mime: str) -> str | None:
40
+ ext = mimetypes.guess_extension(mime, strict=False)
41
+ if ext in _EXTENSION_OVERRIDES:
42
+ return _EXTENSION_OVERRIDES[ext]
43
+ return ext
44
+
45
+
46
+ def _parse_data_url(target: str) -> tuple[str, bytes] | None:
47
+ match = _DATA_URL_PATTERN.match(target)
48
+ if not match:
49
+ return None
50
+ mime = match.group(1) or ""
51
+ if not mime.startswith("image/"):
52
+ return None
53
+ if match.group(2) != ";base64":
54
+ return None
55
+ payload = match.group(3) or ""
56
+ try:
57
+ return mime, base64.b64decode(payload)
58
+ except Exception:
59
+ return None
60
+
61
+
62
+ def _hash_bytes(data: bytes) -> str:
63
+ return hashlib.sha256(data).hexdigest()
64
+
65
+
66
+ def _hash_text(text: str) -> str:
67
+ return hashlib.sha256(text.encode("utf-8", errors="ignore")).hexdigest()
68
+
69
+
70
+ def _hash_file(path: Path) -> str:
71
+ digest = hashlib.sha256()
72
+ with path.open("rb") as handle:
73
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
74
+ digest.update(chunk)
75
+ return digest.hexdigest()
76
+
77
+
78
+ def _split_link_target(raw_link: str) -> tuple[str, str, str, str]:
79
+ link = raw_link.strip()
80
+ if link.startswith("<"):
81
+ end = link.find(">")
82
+ if end != -1:
83
+ return link[1:end], link[end + 1 :], "<", ">"
84
+ parts = link.split()
85
+ if not parts:
86
+ return "", "", "", ""
87
+ target = parts[0]
88
+ suffix = link[len(target) :]
89
+ return target, suffix, "", ""
90
+
91
+
92
+ class _ImageStore:
93
+ def __init__(self, output_dir: Path | None) -> None:
94
+ self.output_dir = output_dir
95
+ self._written: set[str] = set()
96
+ if output_dir:
97
+ output_dir.mkdir(parents=True, exist_ok=True)
98
+
99
+ def add_image(self, mime: str, data: bytes) -> str | None:
100
+ ext = _extension_from_mime(mime)
101
+ if not ext:
102
+ return None
103
+ digest = _hash_bytes(data)
104
+ filename = f"{digest}{ext}"
105
+ if self.output_dir and filename not in self._written:
106
+ dest = self.output_dir / filename
107
+ if not dest.exists():
108
+ dest.write_bytes(data)
109
+ self._written.add(filename)
110
+ return f"images/{filename}"
111
+
112
+
113
+ def _rewrite_markdown_images(text: str, store: _ImageStore) -> str:
114
+ def replace_md(match: re.Match[str]) -> str:
115
+ alt_text = match.group(1)
116
+ raw_link = match.group(2)
117
+ target, suffix, prefix, postfix = _split_link_target(raw_link)
118
+ parsed = _parse_data_url(target)
119
+ if parsed is None:
120
+ return match.group(0)
121
+ mime, data = parsed
122
+ replacement = store.add_image(mime, data)
123
+ if not replacement:
124
+ return match.group(0)
125
+ new_link = f"{prefix}{replacement}{postfix}{suffix}"
126
+ return f"![{alt_text}]({new_link})"
127
+
128
+ text = _IMAGE_PATTERN.sub(replace_md, text)
129
+
130
+ def replace_img(match: re.Match[str]) -> str:
131
+ tag = match.group(0)
132
+ src_match = _SRC_ATTR_PATTERN.search(tag)
133
+ if not src_match:
134
+ return tag
135
+ raw_value = src_match.group(1)
136
+ quote = ""
137
+ if raw_value and raw_value[0] in {"\"", "'"}:
138
+ quote = raw_value[0]
139
+ value = raw_value[1:-1]
140
+ else:
141
+ value = raw_value
142
+ parsed = _parse_data_url(value)
143
+ if parsed is None:
144
+ return tag
145
+ mime, data = parsed
146
+ replacement = store.add_image(mime, data)
147
+ if not replacement:
148
+ return tag
149
+ new_src = f"{quote}{replacement}{quote}" if quote else replacement
150
+ return tag[: src_match.start(1)] + new_src + tag[src_match.end(1) :]
151
+
152
+ return _IMG_TAG_PATTERN.sub(replace_img, text)
153
+
154
+
155
+ def _safe_read_text(path: Path) -> str:
156
+ try:
157
+ return path.read_text(encoding="utf-8")
158
+ except UnicodeDecodeError:
159
+ return path.read_text(encoding="latin-1")
160
+
161
+
162
+ def build_static_assets(
163
+ index: PaperIndex,
164
+ *,
165
+ static_base_url: str | None,
166
+ static_export_dir: Path | None = None,
167
+ allow_empty_base: bool = False,
168
+ ) -> StaticAssetConfig:
169
+ if static_base_url is None:
170
+ if not allow_empty_base:
171
+ return StaticAssetConfig(
172
+ enabled=False,
173
+ base_url=None,
174
+ images_base_url=None,
175
+ pdf_urls={},
176
+ md_urls={},
177
+ translated_md_urls={},
178
+ )
179
+ base_url = ""
180
+ else:
181
+ base_url = _normalize_base_url(static_base_url)
182
+ if not base_url and not allow_empty_base:
183
+ return StaticAssetConfig(
184
+ enabled=False,
185
+ base_url=None,
186
+ images_base_url=None,
187
+ pdf_urls={},
188
+ md_urls={},
189
+ translated_md_urls={},
190
+ )
191
+
192
+ images_base_url = f"{base_url}/images"
193
+
194
+ pdf_urls: dict[str, str] = {}
195
+ md_urls: dict[str, str] = {}
196
+ translated_md_urls: dict[str, dict[str, str]] = {}
197
+
198
+ images_dir = static_export_dir / "images" if static_export_dir else None
199
+ md_dir = static_export_dir / "md" if static_export_dir else None
200
+ md_translate_dir = static_export_dir / "md_translate" if static_export_dir else None
201
+ pdf_dir = static_export_dir / "pdf" if static_export_dir else None
202
+
203
+ store = _ImageStore(images_dir)
204
+
205
+ if md_dir:
206
+ md_dir.mkdir(parents=True, exist_ok=True)
207
+ if md_translate_dir:
208
+ md_translate_dir.mkdir(parents=True, exist_ok=True)
209
+ if pdf_dir:
210
+ pdf_dir.mkdir(parents=True, exist_ok=True)
211
+
212
+ for source_hash, md_path in index.md_path_by_hash.items():
213
+ raw = _safe_read_text(md_path)
214
+ rewritten = _rewrite_markdown_images(raw, store)
215
+ md_hash = _hash_text(rewritten)
216
+ md_urls[source_hash] = f"{base_url}/md/{md_hash}.md"
217
+ if md_dir:
218
+ target = md_dir / f"{md_hash}.md"
219
+ if not target.exists():
220
+ target.write_text(rewritten, encoding="utf-8")
221
+
222
+ for source_hash, translations in index.translated_md_by_hash.items():
223
+ translated_md_urls[source_hash] = {}
224
+ for lang, md_path in translations.items():
225
+ raw = _safe_read_text(md_path)
226
+ rewritten = _rewrite_markdown_images(raw, store)
227
+ md_hash = _hash_text(rewritten)
228
+ translated_md_urls[source_hash][lang] = f"{base_url}/md_translate/{lang}/{md_hash}.md"
229
+ if md_translate_dir:
230
+ lang_dir = md_translate_dir / lang
231
+ lang_dir.mkdir(parents=True, exist_ok=True)
232
+ target = lang_dir / f"{md_hash}.md"
233
+ if not target.exists():
234
+ target.write_text(rewritten, encoding="utf-8")
235
+
236
+ for source_hash, pdf_path in index.pdf_path_by_hash.items():
237
+ pdf_hash = _hash_file(pdf_path)
238
+ pdf_urls[source_hash] = f"{base_url}/pdf/{pdf_hash}.pdf"
239
+ if pdf_dir:
240
+ target = pdf_dir / f"{pdf_hash}.pdf"
241
+ if not target.exists():
242
+ target.write_bytes(pdf_path.read_bytes())
243
+
244
+ return StaticAssetConfig(
245
+ enabled=True,
246
+ base_url=base_url,
247
+ images_base_url=images_base_url,
248
+ pdf_urls=pdf_urls,
249
+ md_urls=md_urls,
250
+ translated_md_urls=translated_md_urls,
251
+ )
252
+
253
+
254
+ def resolve_asset_urls(
255
+ index: PaperIndex,
256
+ source_hash: str,
257
+ asset_config: StaticAssetConfig | None,
258
+ *,
259
+ prefer_local: bool = False,
260
+ ) -> dict[str, Any]:
261
+ """Resolve asset URLs for a paper based on static asset config or local endpoints."""
262
+ if prefer_local:
263
+ translations = index.translated_md_by_hash.get(source_hash, {})
264
+ images_base_url = asset_config.images_base_url if asset_config and asset_config.enabled else None
265
+ return {
266
+ "pdf_url": f"/api/pdf/{source_hash}" if source_hash in index.pdf_path_by_hash else None,
267
+ "md_url": f"/api/dev/markdown/{source_hash}" if source_hash in index.md_path_by_hash else None,
268
+ "md_translated_url": {
269
+ lang: f"/api/dev/markdown/{source_hash}?lang={lang}" for lang in translations
270
+ },
271
+ "images_base_url": images_base_url,
272
+ }
273
+ if asset_config and asset_config.enabled:
274
+ return {
275
+ "pdf_url": asset_config.pdf_urls.get(source_hash),
276
+ "md_url": asset_config.md_urls.get(source_hash),
277
+ "md_translated_url": asset_config.translated_md_urls.get(source_hash, {}),
278
+ "images_base_url": asset_config.images_base_url,
279
+ }
280
+
281
+ translations = index.translated_md_by_hash.get(source_hash, {})
282
+ return {
283
+ "pdf_url": f"/api/pdf/{source_hash}" if source_hash in index.pdf_path_by_hash else None,
284
+ "md_url": f"/api/dev/markdown/{source_hash}" if source_hash in index.md_path_by_hash else None,
285
+ "md_translated_url": {
286
+ lang: f"/api/dev/markdown/{source_hash}?lang={lang}" for lang in translations
287
+ },
288
+ "images_base_url": None,
289
+ }
@@ -1,7 +1,7 @@
1
1
  {% extends "base.html" %}
2
2
 
3
3
  {% block extra_head %}
4
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" />
4
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.27/dist/katex.min.css" />
5
5
  <style>
6
6
  #content img {
7
7
  max-width: 100%;
@@ -162,9 +162,10 @@
162
162
  <button id="outlineToggle" class="inline-flex h-9 items-center justify-center rounded-md bg-slate-900 px-3 text-sm text-white shadow hover:bg-slate-800">☰ Outline</button>
163
163
  </div>
164
164
  {% endif %}
165
- <article id="content" class="content">{{ body_html|safe }}</article>
165
+ <article id="content" class="content"{% if source_markdown_url %}{% if body_html %} data-raw-markdown-url="{{ source_markdown_url }}"{% else %} data-markdown-url="{{ source_markdown_url }}"{% endif %}{% endif %}{% if images_base_url %} data-images-base-url="{{ images_base_url }}"{% endif %}>{{ body_html|safe }}</article>
166
+ <div id="markdownStatus" class="muted" style="margin-top:10px;">{% if body_html %}Loading raw markdown...{% else %}Loading markdown...{% endif %}</div>
166
167
  <details style="margin-top:12px;"><summary>Raw markdown</summary>
167
- <pre><code>{{ raw_content|escape }}</code></pre>
168
+ <pre><code id="rawMarkdown"></code></pre>
168
169
  </details>
169
170
  {% endif %}
170
171
 
@@ -200,9 +201,10 @@
200
201
  <button id="outlineToggle" class="inline-flex h-9 items-center justify-center rounded-md bg-slate-900 px-3 text-sm text-white shadow hover:bg-slate-800">☰ Outline</button>
201
202
  </div>
202
203
  {% endif %}
203
- <article id="content" class="content">{{ body_html|safe }}</article>
204
+ <article id="content" class="content"{% if translated_markdown_url %}{% if body_html %} data-raw-markdown-url="{{ translated_markdown_url }}"{% else %} data-markdown-url="{{ translated_markdown_url }}"{% endif %}{% endif %}{% if images_base_url %} data-images-base-url="{{ images_base_url }}"{% endif %}>{{ body_html|safe }}</article>
205
+ <div id="markdownStatus" class="muted" style="margin-top:10px;">{% if body_html %}Loading raw markdown...{% else %}Loading markdown...{% endif %}</div>
204
206
  <details style="margin-top:12px;"><summary>Raw markdown</summary>
205
- <pre><code>{{ raw_content|escape }}</code></pre>
207
+ <pre><code id="rawMarkdown"></code></pre>
206
208
  </details>
207
209
  {% endif %}
208
210
 
@@ -251,82 +253,57 @@
251
253
 
252
254
  {% block extra_scripts %}
253
255
  {# CDN scripts for markdown rendering #}
254
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js"></script>
255
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js"></script>
256
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.16.27/dist/katex.min.js"></script>
257
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.16.27/dist/contrib/auto-render.min.js"></script>
256
258
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
257
259
  <script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.15.4/dist/browser/index.min.js"></script>
258
- <script src="https://cdn.jsdelivr.net/npm/markmap-view@0.15.4/dist/index.min.js"></script>
260
+ <script src="https://cdn.jsdelivr.net/npm/markmap-view@0.15.4/dist/browser/index.min.js"></script>
261
+ <script src="https://cdn.jsdelivr.net/npm/marked@12.0.1/marked.min.js"></script>
262
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
259
263
 
260
264
  {# PDF.js for PDF view #}
261
265
  {% if current_view == 'pdf' %}
262
- <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.min.js"></script>
263
- {% endif %}
264
-
265
- {# Main detail JavaScript #}
266
- <script src="/static/js/detail.js"></script>
267
-
268
- {# Outline functionality #}
269
- {% if show_outline %}
270
266
  <script>
271
267
  (function() {
272
- function initOutline() {
273
- var content = document.getElementById('content');
274
- if (!content) return;
275
- var headings = content.querySelectorAll('h2, h3, h4');
276
- if (headings.length === 0) return;
277
-
278
- var outline = document.getElementById('outline');
279
- var toggle = document.getElementById('outlineToggle');
280
- var close = document.getElementById('outlineClose');
281
- var outlineContent = document.getElementById('outlineContent');
282
-
283
- if (!outline || !toggle || !close || !outlineContent) return;
284
-
285
- for (var i = 0; i < headings.length; i++) {
286
- var h = headings[i];
287
- if (!h.id) h.id = 'heading-' + i;
288
- var a = document.createElement('a');
289
- a.href = '#' + h.id;
290
- a.textContent = h.textContent.trim();
291
- a.className = 'outline-' + h.tagName.toLowerCase();
292
- outlineContent.appendChild(a);
293
- }
294
-
295
- toggle.addEventListener('click', function() {
296
- outline.style.display = 'block';
297
- toggle.style.display = 'none';
298
- document.body.classList.add('outline-open');
299
- });
300
-
301
- close.addEventListener('click', function() {
302
- outline.style.display = 'none';
303
- toggle.style.display = 'block';
304
- document.body.classList.remove('outline-open');
305
- });
306
-
307
- var savedState = sessionStorage.getItem('outlineVisible');
308
- if (savedState === 'true') {
309
- outline.style.display = 'block';
310
- toggle.style.display = 'none';
311
- document.body.classList.add('outline-open');
312
- } else {
313
- toggle.style.display = 'block';
314
- document.body.classList.remove('outline-open');
268
+ var primary = {{ pdfjs_script_url|tojson }};
269
+ var fallback = "/pdfjs/build/pdf.js";
270
+ if (!primary) {
271
+ primary = fallback;
272
+ fallback = "";
273
+ }
274
+ window.PDFJS_SCRIPT_SRC = primary;
275
+ window.PDFJS_WORKER_SRC = {{ pdfjs_worker_url|tojson }} || "/pdfjs/build/pdf.worker.js";
276
+ function notifyLoaded() {
277
+ try {
278
+ window.dispatchEvent(new Event('pdfjs:loaded'));
279
+ } catch (err) {
280
+ var event = document.createEvent('Event');
281
+ event.initEvent('pdfjs:loaded', true, true);
282
+ window.dispatchEvent(event);
315
283
  }
316
-
317
- var observer = new MutationObserver(function() {
318
- var isVisible = outline.style.display === 'block';
319
- sessionStorage.setItem('outlineVisible', String(isVisible));
320
- });
321
- observer.observe(outline, { attributes: true, attributeFilter: ['style'] });
322
284
  }
323
-
324
- if (document.readyState === 'loading') {
325
- document.addEventListener('DOMContentLoaded', initOutline);
285
+ function loadScript(src, onerror) {
286
+ var script = document.createElement('script');
287
+ script.src = src;
288
+ script.defer = true;
289
+ script.onload = notifyLoaded;
290
+ if (onerror) script.onerror = onerror;
291
+ document.head.appendChild(script);
292
+ }
293
+ if (fallback && primary !== fallback) {
294
+ loadScript(primary, function() { loadScript(fallback); });
326
295
  } else {
327
- initOutline();
296
+ loadScript(primary);
328
297
  }
329
298
  })();
330
299
  </script>
331
300
  {% endif %}
301
+
302
+ {# Main detail JavaScript #}
303
+ <script src="/static/js/detail.js"></script>
304
+
305
+ {# Outline functionality #}
306
+ {% if show_outline %}
307
+ <script src="/static/js/outline.js"></script>
308
+ {% endif %}
332
309
  {% endblock %}
@@ -1,7 +1,7 @@
1
1
  {% extends "base.html" %}
2
2
 
3
3
  {% block extra_head %}
4
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" />
4
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.27/dist/katex.min.css" />
5
5
  <style>
6
6
  #content img {
7
7
  max-width: 100%;
@@ -108,7 +108,7 @@
108
108
  {% endblock %}
109
109
 
110
110
  {% block extra_scripts %}
111
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js"></script>
112
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js"></script>
111
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.16.27/dist/katex.min.js"></script>
112
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.16.27/dist/contrib/auto-render.min.js"></script>
113
113
  <script src="/static/js/index.js"></script>
114
114
  {% endblock %}
@@ -7,7 +7,7 @@ for installed package compatibility.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from urllib.parse import quote
10
+ from urllib.parse import urlencode
11
11
 
12
12
  from jinja2 import Environment, FileSystemLoader, PackageLoader
13
13
 
@@ -72,14 +72,17 @@ def render_template(template_name: str, **context) -> str:
72
72
  return template.render(**context)
73
73
 
74
74
 
75
- def build_pdfjs_viewer_url(pdf_url: str) -> str:
75
+ def build_pdfjs_viewer_url(pdf_url: str, *, cdn_base_url: str | None = None) -> str:
76
76
  """Build a PDF.js viewer URL for the given PDF URL.
77
77
 
78
78
  Args:
79
79
  pdf_url: The URL of the PDF file
80
+ cdn_base_url: Optional CDN base URL for PDF.js assets
80
81
 
81
82
  Returns:
82
83
  Full URL to the PDF.js viewer with the PDF file as a query parameter
83
84
  """
84
- encoded = quote(pdf_url, safe="")
85
- return f"{PDFJS_VIEWER_PATH}?file={encoded}"
85
+ params = {"file": pdf_url, "allow_origin": "1"}
86
+ if cdn_base_url:
87
+ params["cdn"] = cdn_base_url.rstrip("/")
88
+ return f"{PDFJS_VIEWER_PATH}?{urlencode(params)}"