Fix viewer: safe JSON embed so search cards render again.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -216,12 +216,14 @@ def viewer(
|
|||||||
home: Optional[str] = Query(None),
|
home: Optional[str] = Query(None),
|
||||||
):
|
):
|
||||||
sections = fetch_sections(prefix)
|
sections = fetch_sections(prefix)
|
||||||
|
# Escape < so </script> inside text_html cannot break the page script tag
|
||||||
|
sections_json = json.dumps(sections, ensure_ascii=False, default=str).replace("<", "\\u003c")
|
||||||
home_url = "/home-image" if (home or HOME_IMAGE or (BASE_DIR.parent / "Bairaci.png").exists()) else None
|
home_url = "/home-image" if (home or HOME_IMAGE or (BASE_DIR.parent / "Bairaci.png").exists()) else None
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"viewer.html",
|
"viewer.html",
|
||||||
{
|
{
|
||||||
"sections_json": json.dumps(sections, ensure_ascii=False, default=str),
|
"sections_json": sections_json,
|
||||||
"section_count": len(sections),
|
"section_count": len(sections),
|
||||||
"prefix": prefix or "",
|
"prefix": prefix or "",
|
||||||
"home_url": home_url,
|
"home_url": home_url,
|
||||||
|
|||||||
@@ -335,21 +335,44 @@
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
|
<script type="application/json" id="sections-data">{{ sections_json | safe }}</script>
|
||||||
<script>
|
<script>
|
||||||
const ALL = {{ sections_json | safe }};
|
let ALL = [];
|
||||||
|
try {
|
||||||
|
ALL = JSON.parse(document.getElementById('sections-data').textContent || '[]');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('sections JSON parse failed', e);
|
||||||
|
ALL = [];
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('total-count').textContent = ALL.length + ' секции';
|
function esc(s) {
|
||||||
renderEditor(ALL);
|
return String(s ?? '')
|
||||||
renderKwCloud();
|
.replace(/&/g,'&').replace(/</g,'<')
|
||||||
doSearch();
|
.replace(/>/g,'>').replace(/"/g,'"');
|
||||||
renderGenerator();
|
}
|
||||||
|
function shortPath(p) {
|
||||||
|
if (!p) return '';
|
||||||
|
const parts = String(p).replace(/\\/g,'/').split('/');
|
||||||
|
return parts.slice(-2).join('/');
|
||||||
|
}
|
||||||
|
function toast(msg, isErr) {
|
||||||
|
const el = document.getElementById('toast');
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = msg;
|
||||||
|
el.classList.toggle('error', !!isErr);
|
||||||
|
el.classList.add('show');
|
||||||
|
clearTimeout(toast._t);
|
||||||
|
toast._t = setTimeout(() => el.classList.remove('show'), 2800);
|
||||||
|
}
|
||||||
|
|
||||||
function switchTab(name) {
|
function switchTab(name) {
|
||||||
const order = [{% if home_url %}'home',{% endif %}'editor','search','generator'];
|
const order = [{% if home_url %}'home',{% endif %}'editor','search','generator'];
|
||||||
document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', order[i] === name));
|
document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', order[i] === name));
|
||||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||||
document.getElementById('tab-' + name).classList.add('active');
|
const panel = document.getElementById('tab-' + name);
|
||||||
|
if (panel) panel.classList.add('active');
|
||||||
if (name === 'generator') renderGenerator();
|
if (name === 'generator') renderGenerator();
|
||||||
|
if (name === 'search') doSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startRescan() {
|
async function startRescan() {
|
||||||
@@ -703,28 +726,24 @@ ${bodyAbs}
|
|||||||
</body></html>`;
|
</body></html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── UTILS ─────────────────────────────
|
|
||||||
function esc(s) {
|
|
||||||
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
||||||
}
|
|
||||||
function shortPath(p) {
|
|
||||||
if (!p) return '';
|
|
||||||
const parts = p.replace(/\\/g,'/').split('/');
|
|
||||||
return parts.slice(-2).join('/');
|
|
||||||
}
|
|
||||||
function toast(msg, isError) {
|
|
||||||
const el = document.getElementById('toast');
|
|
||||||
el.textContent = msg;
|
|
||||||
el.classList.toggle('error', !!isError);
|
|
||||||
el.classList.add('show');
|
|
||||||
setTimeout(() => el.classList.remove('show'), 3000);
|
|
||||||
}
|
|
||||||
function download(filename, content, mime) {
|
function download(filename, content, mime) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(new Blob([content], {type: mime}));
|
a.href = URL.createObjectURL(new Blob([content], {type: mime}));
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── INIT ─────────────────────────────
|
||||||
|
document.getElementById('total-count').textContent = ALL.length + ' секции';
|
||||||
|
try {
|
||||||
|
renderEditor(ALL);
|
||||||
|
renderKwCloud();
|
||||||
|
doSearch();
|
||||||
|
renderGenerator();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('init failed', e);
|
||||||
|
toast('Грешка при зареждане на UI: ' + e.message, true);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user