Spaces:
Running
Open in Overleaf posts the project itself; tests for the formatter and finder tabs
Browse files- formatter_tab.overleaf_html: the LaTeX zip is sent to Overleaf's API
(overleaf.com/devs) in a POST form as snip_uri=data:application/zip;
base64, with engine=pdflatex and main_document=main.tex. Overleaf no
longer has to download it from the Space, so the button works on a
private or sleeping Space and on a local run.
- A project above OVERLEAF_EMBED_MB (6) goes by its public address as
before; without one, only the manual upload is explained.
- tests/test_tools_tabs.py: formatting a Word manuscript (Word, LaTeX
zip with main.tex at the top level, report), the three Overleaf cases,
finder job options, shortlist table, input checks, both tabs wired in.
Checked in a browser that the form renders inside the app; the button
itself was not pressed (it creates a project on Overleaf).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- formatter_tab.py +29 -6
- tests/test_tools_tabs.py +149 -0
|
@@ -6,8 +6,9 @@ house style, the journal's Word template or its author guidelines, and writes th
|
|
| 6 |
a compliance report. Equations, algorithms and complex tables are carried over untouched.
|
| 7 |
|
| 8 |
Overleaf: the LaTeX project is a zip with main.tex at its top level, refs.bib and figures/, using classes that
|
| 9 |
-
Overleaf's TeX Live has (IEEEtran, elsarticle, llncs, sn-jnl ...). "Open in Overleaf"
|
| 10 |
-
|
|
|
|
| 11 |
"""
|
| 12 |
import html
|
| 13 |
import os
|
|
@@ -52,15 +53,37 @@ def public_url(path):
|
|
| 52 |
return f"https://{host}/gradio_api/file={quote(str(path).replace(os.sep, '/'), safe='/:')}"
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def overleaf_html(zip_path):
|
| 56 |
-
""""Open in Overleaf" for a LaTeX project zip
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
'<code>refs.bib</code> and the figures in <code>figures/</code>; compile with pdfLaTeX.</div>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
url = public_url(zip_path)
|
| 61 |
if not url:
|
| 62 |
return manual
|
| 63 |
-
link = "
|
| 64 |
return (f'<a class="cs-dl" target="_blank" rel="noopener" href="{html.escape(link, quote=True)}">'
|
| 65 |
f'🍃 Open in Overleaf</a>' + manual)
|
| 66 |
|
|
|
|
| 6 |
a compliance report. Equations, algorithms and complex tables are carried over untouched.
|
| 7 |
|
| 8 |
Overleaf: the LaTeX project is a zip with main.tex at its top level, refs.bib and figures/, using classes that
|
| 9 |
+
Overleaf's TeX Live has (IEEEtran, elsarticle, llncs, sn-jnl ...). "Open in Overleaf" posts that zip to Overleaf
|
| 10 |
+
itself (overleaf.com/devs: snip_uri as a base64 data URL), so it works on a private Space and on a local run; only
|
| 11 |
+
a very large project is handed over by its address on the Space (SPACE_HOST) instead.
|
| 12 |
"""
|
| 13 |
import html
|
| 14 |
import os
|
|
|
|
| 53 |
return f"https://{host}/gradio_api/file={quote(str(path).replace(os.sep, '/'), safe='/:')}"
|
| 54 |
|
| 55 |
|
| 56 |
+
OVERLEAF = "https://www.overleaf.com/docs" # Overleaf's "Open in Overleaf" API (overleaf.com/devs)
|
| 57 |
+
# A project up to this size is sent to Overleaf inside the button itself; a larger one by its address on the Space.
|
| 58 |
+
OVERLEAF_EMBED_MB = float(os.getenv("OVERLEAF_EMBED_MB", "6"))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
def overleaf_html(zip_path):
|
| 62 |
+
""""Open in Overleaf" for a LaTeX project zip.
|
| 63 |
+
|
| 64 |
+
Overleaf's API takes the project in `snip_uri`, either as an address it downloads or as the file itself in a
|
| 65 |
+
base64 data URL. The file itself is the reliable way: Overleaf never has to reach this Space (which may be
|
| 66 |
+
private, asleep, or a local run), so the zip is posted in a form. Only a project too large to post is handed
|
| 67 |
+
over by its public address; with neither possible, the manual upload is explained."""
|
| 68 |
+
manual = ('<div class="cs-gauge-cap" style="text-align:left">Or by hand in Overleaf: <b>New Project → Upload '
|
| 69 |
+
'Project</b> and choose the LaTeX .zip. It opens with <code>main.tex</code>, the references in '
|
| 70 |
'<code>refs.bib</code> and the figures in <code>figures/</code>; compile with pdfLaTeX.</div>')
|
| 71 |
+
if not zip_path or not os.path.exists(zip_path):
|
| 72 |
+
return ""
|
| 73 |
+
size_mb = os.path.getsize(zip_path) / (1024 * 1024)
|
| 74 |
+
if size_mb <= OVERLEAF_EMBED_MB:
|
| 75 |
+
import base64
|
| 76 |
+
data = "data:application/zip;base64," + base64.b64encode(Path(zip_path).read_bytes()).decode("ascii")
|
| 77 |
+
return (f'<form action="{OVERLEAF}" method="post" target="_blank" style="margin:0">'
|
| 78 |
+
f'<input type="hidden" name="snip_uri" value="{data}">'
|
| 79 |
+
'<input type="hidden" name="engine" value="pdflatex">'
|
| 80 |
+
'<input type="hidden" name="main_document" value="main.tex">'
|
| 81 |
+
'<button type="submit" class="cs-dl" style="border:0;cursor:pointer;font:inherit">'
|
| 82 |
+
'🍃 Open in Overleaf</button></form>' + manual)
|
| 83 |
url = public_url(zip_path)
|
| 84 |
if not url:
|
| 85 |
return manual
|
| 86 |
+
link = f"{OVERLEAF}?engine=pdflatex&main_document=main.tex&snip_uri=" + quote(url, safe="")
|
| 87 |
return (f'<a class="cs-dl" target="_blank" rel="noopener" href="{html.escape(link, quote=True)}">'
|
| 88 |
f'🍃 Open in Overleaf</a>' + manual)
|
| 89 |
|
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Journal Formatter and Journal Finder tabs (no network: the journal lookup and the search itself are not run)."""
|
| 2 |
+
import base64
|
| 3 |
+
import io
|
| 4 |
+
import json
|
| 5 |
+
import sys
|
| 6 |
+
import zipfile
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
import finder_tab as FND
|
| 12 |
+
import formatter_tab as FMT
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 15 |
+
progress = lambda *a, **k: None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _manuscript(tmp_path):
|
| 19 |
+
from docx import Document
|
| 20 |
+
d = Document()
|
| 21 |
+
d.add_heading("A Hybrid Deep Learning Framework for Intrusion Detection", 0)
|
| 22 |
+
d.add_paragraph("Ravi Kumar, Priya Sharma")
|
| 23 |
+
d.add_paragraph("Abstract: This study proposes a hybrid model that reaches 98.4% accuracy on a public dataset.")
|
| 24 |
+
d.add_heading("Introduction", 1)
|
| 25 |
+
d.add_paragraph("Medical devices are increasingly connected (Smith, 2020). Prior work used shallow models.")
|
| 26 |
+
d.add_heading("References", 1)
|
| 27 |
+
d.add_paragraph("Smith, J. (2020). Deep learning for intrusion detection. Journal of Network Security, 12(3), "
|
| 28 |
+
"45-67.")
|
| 29 |
+
p = tmp_path / "my paper.docx"
|
| 30 |
+
d.save(str(p))
|
| 31 |
+
return str(p)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# ---------------------------------------------------------------- formatter
|
| 35 |
+
def test_formatter_engine_is_available():
|
| 36 |
+
assert FMT.AVAILABLE, FMT.WHY
|
| 37 |
+
assert len(FMT._preset_choices()) > 10 and FMT._preset_key(FMT.AUTO) is None
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_format_gives_word_latex_report_and_an_overleaf_button(tmp_path):
|
| 41 |
+
style = FMT.PRESETS["ieee_journal"]["name"]
|
| 42 |
+
status, docx, zip_, md, overleaf, report = FMT.format_doc(_manuscript(tmp_path), "", style, None, "", "", True,
|
| 43 |
+
True, False, FMT.REF_STYLES[0], FMT.CITE_MODES[0],
|
| 44 |
+
progress)
|
| 45 |
+
assert "Formatted as" in status and "checks passed" in status
|
| 46 |
+
assert docx.endswith("my_paper_formatted.docx") and Path(docx).stat().st_size > 5000
|
| 47 |
+
names = zipfile.ZipFile(zip_).namelist()
|
| 48 |
+
assert "main.tex" in names and not any(n.startswith("/") for n in names) # main.tex at the top level
|
| 49 |
+
main = zipfile.ZipFile(zip_).read("main.tex").decode("utf-8")
|
| 50 |
+
assert "\\documentclass" in main and "IEEEtran" in main and "\\begin{document}" in main
|
| 51 |
+
assert Path(md).read_text(encoding="utf-8").startswith("# Formatting report") and "Compliance" in report
|
| 52 |
+
assert 'action="https://www.overleaf.com/docs"' in overleaf and 'method="post"' in overleaf
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_format_needs_a_manuscript_and_a_target(tmp_path):
|
| 56 |
+
import gradio as gr
|
| 57 |
+
with pytest.raises(gr.Error):
|
| 58 |
+
FMT.format_doc(None, "IEEE Access", FMT.AUTO, None, "", "", True, True, False, "", "", progress)
|
| 59 |
+
with pytest.raises(gr.Error):
|
| 60 |
+
FMT.format_doc(_manuscript(tmp_path), "", FMT.AUTO, None, "", "", True, True, False, "", "", progress)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _zip(tmp_path, kb=1):
|
| 64 |
+
p = tmp_path / "paper_latex.zip"
|
| 65 |
+
with zipfile.ZipFile(p, "w") as z:
|
| 66 |
+
z.writestr("main.tex", "\\documentclass{article}\\begin{document}Hi\\end{document}")
|
| 67 |
+
z.writestr("figures/pad.bin", bytes(range(256)) * 4 * kb, compress_type=zipfile.ZIP_STORED)
|
| 68 |
+
return str(p)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_overleaf_button_posts_the_project_itself(tmp_path):
|
| 72 |
+
p = _zip(tmp_path)
|
| 73 |
+
h = FMT.overleaf_html(p)
|
| 74 |
+
assert '<form action="https://www.overleaf.com/docs" method="post" target="_blank"' in h
|
| 75 |
+
assert 'name="engine" value="pdflatex"' in h and 'name="main_document" value="main.tex"' in h
|
| 76 |
+
data = h.split('name="snip_uri" value="', 1)[1].split('"', 1)[0]
|
| 77 |
+
assert data.startswith("data:application/zip;base64,")
|
| 78 |
+
back = zipfile.ZipFile(io.BytesIO(base64.b64decode(data.split(",", 1)[1])))
|
| 79 |
+
assert "main.tex" in back.namelist() # exactly the project the customer downloads
|
| 80 |
+
assert "Upload Project" in h # and the manual route is always explained
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def test_a_large_project_goes_by_its_address_or_by_hand(tmp_path, monkeypatch):
|
| 84 |
+
p = _zip(tmp_path, kb=64)
|
| 85 |
+
monkeypatch.setattr(FMT, "OVERLEAF_EMBED_MB", 0.01)
|
| 86 |
+
monkeypatch.setenv("SPACE_HOST", "rasheed-ai-writing-suite.hf.space")
|
| 87 |
+
h = FMT.overleaf_html(p)
|
| 88 |
+
assert "<form" not in h and "snip_uri=https%3A%2F%2Frasheed-ai-writing-suite.hf.space%2Fgradio_api%2Ffile%3D" in h
|
| 89 |
+
assert "main_document=main.tex" in h and "engine=pdflatex" in h
|
| 90 |
+
monkeypatch.delenv("SPACE_HOST")
|
| 91 |
+
monkeypatch.delenv("PUBLIC_HOST", raising=False)
|
| 92 |
+
h = FMT.overleaf_html(p)
|
| 93 |
+
assert "Open in Overleaf" not in h and "Upload Project" in h
|
| 94 |
+
assert FMT.overleaf_html(None) == ""
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# ---------------------------------------------------------------- finder
|
| 98 |
+
def test_finder_files_and_data_are_in_place():
|
| 99 |
+
assert FND.AVAILABLE
|
| 100 |
+
data = {f.name for f in (ROOT / "finder" / "data").iterdir()}
|
| 101 |
+
assert {"ext_list_Jul_2026.xlsx", "scimagojr_2025.csv", "J_Medline.txt", "core_conference_ranks.csv"} <= data
|
| 102 |
+
assert any("ABDC" in n for n in data)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def test_job_options_map_to_the_pipeline_flags():
|
| 106 |
+
sys.path.insert(0, str(ROOT / "finder"))
|
| 107 |
+
import run_job
|
| 108 |
+
a = run_job.Args(title="A study of things", terms=["x"], limit=20, top=5, acceptance=False, turnaround=True,
|
| 109 |
+
semantic=False, apc=True, quartiles=[1, 2], require_scopus=True, abdc_ranks=["A*", "A"],
|
| 110 |
+
fast_days=60, sort_by="speed", special_issues=False)
|
| 111 |
+
assert a.title == ["A", "study", "of", "things"] and a.term == ["x"] and a.limit == 20 and a.top == 5
|
| 112 |
+
assert a.no_scrape is True and a.no_turnaround is False and a.no_semantic is True and a.no_apc is False
|
| 113 |
+
assert a.fast is True and a.quartile == [1, 2] and a.abdc_ranks == ["A*", "A"] and a.fast_days == 60
|
| 114 |
+
assert a.sort_by == "speed" and callable(a.progress)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def test_shortlist_table_is_readable(tmp_path):
|
| 118 |
+
csv = tmp_path / "shortlist.csv"
|
| 119 |
+
csv.write_text("display_name,publisher,quartile,quartile_is_proxy,abdc_rank,in_pubmed,acceptance_rate_used,"
|
| 120 |
+
"acceptance_days,acceptance_speed,apc_status,apc_usd_est,term_matches,opportunity_score,homepage,"
|
| 121 |
+
"openalex_id\n"
|
| 122 |
+
"Computers & Security,Elsevier,1,False,,False,0.21,112,Moderate,paid,3200,2,71.234,"
|
| 123 |
+
"https://example.org/cs,S1\n"
|
| 124 |
+
"Journal of Things,Springer,2,True,A,True,,,,no APC,,1,55.5,https://example.org/jt,S2\n",
|
| 125 |
+
encoding="utf-8")
|
| 126 |
+
view, n = FND._table(csv)
|
| 127 |
+
assert n == 2 and list(view.columns)[:3] == ["Journal", "Publisher", "Quartile"] and "openalex_id" not in view
|
| 128 |
+
r1, r2 = view.iloc[0], view.iloc[1]
|
| 129 |
+
assert r1["Quartile"] == "Q1" and r1["Acceptance rate"] == "21%" and r1["APC"] == "$3,200" and r1["PubMed"] == "—"
|
| 130 |
+
assert r2["Quartile"] == "Q2 (estimate)" and r2["APC"] == "no APC" and r2["PubMed"] == "yes" and r2["ABDC"] == "A"
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def test_find_asks_for_a_title_and_a_subject():
|
| 134 |
+
import gradio as gr
|
| 135 |
+
blank = ["", [], "", ["Scopus"], [], [], 40, 10, 3, False, 90, "Best fit", True, True, True, True, 0, False]
|
| 136 |
+
with pytest.raises(gr.Error):
|
| 137 |
+
FND.find_journals(*blank, progress=progress)
|
| 138 |
+
blank[0] = "A hybrid deep learning framework for intrusion detection"
|
| 139 |
+
with pytest.raises(gr.Error):
|
| 140 |
+
FND.find_journals(*blank, progress=progress) # title but no subject kept
|
| 141 |
+
with pytest.raises(gr.Error):
|
| 142 |
+
FND.detect_terms("too short")
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_both_tabs_are_part_of_the_app():
|
| 146 |
+
app = pytest.importorskip("app")
|
| 147 |
+
labels = json.dumps(app.demo.get_config_file(), default=str)
|
| 148 |
+
assert "Format my manuscript" in labels and "Find journals" in labels and "Work out the subject" in labels
|
| 149 |
+
assert "cs-go-formatter" in labels and "cs-go-finder" in labels
|