unknown Claude Opus 5.5 commited on
Commit
e7bb050
·
1 Parent(s): 7ee2a84

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>

Files changed (2) hide show
  1. formatter_tab.py +29 -6
  2. tests/test_tools_tabs.py +149 -0
formatter_tab.py CHANGED
@@ -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" hands that zip to Overleaf
10
- by its address on this Space, so it only appears where the Space is reachable from the internet (SPACE_HOST).
 
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, or how to upload it by hand."""
57
- manual = ('<div class="cs-gauge-cap" style="text-align:left">In Overleaf: <b>New Project → Upload Project</b> '
58
- 'and choose the LaTeX .zip. It opens with <code>main.tex</code>, the references in '
 
 
 
 
 
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 = "https://www.overleaf.com/docs?engine=pdflatex&snip_uri=" + quote(url, safe="")
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
 
tests/test_tools_tabs.py ADDED
@@ -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