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.
- deepresearch_flow/paper/db.py +34 -0
- deepresearch_flow/paper/web/app.py +106 -1
- deepresearch_flow/paper/web/constants.py +5 -4
- deepresearch_flow/paper/web/handlers/__init__.py +2 -1
- deepresearch_flow/paper/web/handlers/api.py +55 -0
- deepresearch_flow/paper/web/handlers/pages.py +105 -25
- deepresearch_flow/paper/web/markdown.py +60 -0
- deepresearch_flow/paper/web/pdfjs/web/viewer.html +57 -5
- deepresearch_flow/paper/web/pdfjs/web/viewer.js +5 -1
- deepresearch_flow/paper/web/static/js/detail.js +494 -125
- deepresearch_flow/paper/web/static/js/outline.js +48 -34
- deepresearch_flow/paper/web/static_assets.py +289 -0
- deepresearch_flow/paper/web/templates/detail.html +46 -69
- deepresearch_flow/paper/web/templates/index.html +3 -3
- deepresearch_flow/paper/web/templates.py +7 -4
- deepresearch_flow/recognize/cli.py +805 -26
- deepresearch_flow/recognize/katex_check.js +29 -0
- deepresearch_flow/recognize/math.py +719 -0
- deepresearch_flow/recognize/mermaid.py +690 -0
- {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/METADATA +117 -4
- {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/RECORD +25 -21
- {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/WHEEL +0 -0
- {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/entry_points.txt +0 -0
- {deepresearch_flow-0.4.0.dist-info → deepresearch_flow-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
4
|
-
const content = document.getElementById('content');
|
|
5
|
-
if (!content) return;
|
|
3
|
+
var bound = false;
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
function buildOutline() {
|
|
6
|
+
var content = document.getElementById('content');
|
|
7
|
+
if (!content) return;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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""
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
255
|
-
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
112
|
-
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
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)}"
|