"""Journal Formatter tab: arrange a manuscript into a journal's format (Word + an Overleaf-ready LaTeX project). The work is done by the FormatMatch engine in ./formatmatch: it reads the manuscript (.docx, .pdf, LaTeX .tex / .zip - an Overleaf "Download source" zip works -, Markdown), builds the target format from a journal name, a publisher house style, the journal's Word template or its author guidelines, and writes the paper again in that format with a compliance report. Equations, algorithms and complex tables are carried over untouched. Overleaf: the LaTeX project is a zip with main.tex at its top level, refs.bib and figures/, using classes that Overleaf's TeX Live has (IEEEtran, elsarticle, llncs, sn-jnl ...). The customer downloads it and uploads it to Overleaf himself (New Project -> Upload Project); the page explains how. """ import html import os import re import tempfile import time from pathlib import Path import gradio as gr from tools_common import download_row TOOL = "Journal Formatter" # job key: a reconnecting page picks up its own running job try: from formatmatch.engine import (FormatSource, build_spec, fetch_guidelines_url, format_manuscript, guidelines_text_from_file, learn_template) from formatmatch.read_any import SUPPORTED from formatmatch.spec import PRESETS, lookup_journal, parse_guidelines AVAILABLE, WHY = True, "" except Exception as _err: # the suite must start even if this tool cannot load AVAILABLE, WHY = False, f"{type(_err).__name__}: {_err}" PRESETS, SUPPORTED = {}, [] OUT_DIR = Path(tempfile.gettempdir()) / "cs_outputs" AUTO = "Recognise from the journal name" REF_STYLES = ["(from the journal)", "ieee", "apa", "harvard", "emerald-harvard", "vancouver", "ama", "chicago", "springer-basic", "springer-vancouver", "lncs", "mdpi", "elsevier-numeric", "mla"] CITE_MODES = ["(from the journal)", "numeric", "superscript", "author-year", "author-year-nocomma"] MAX_MB = float(os.getenv("FORMATTER_MAX_MB", "40")) def _preset_choices(): return [AUTO] + [PRESETS[k]["name"] for k in PRESETS] def _preset_key(label): return next((k for k in PRESETS if PRESETS[k]["name"] == label), None) def overleaf_html(zip_path): """How to use the LaTeX project in Overleaf. The customer downloads the zip and uploads it there himself: there is no button that sends it to Overleaf.""" if not zip_path or not os.path.exists(zip_path): return "" return ('
πŸƒ To use it in Overleaf: download the LaTeX ' 'project above, then in Overleaf choose New Project β†’ Upload Project and select the .zip. It ' 'opens with main.tex, the references in refs.bib and the figures in ' 'figures/; compile with pdfLaTeX.
') def _data(path): p = Path(str(path)) if p.stat().st_size > MAX_MB * 1024 * 1024: raise gr.Error(f"That file is larger than {MAX_MB:.0f} MB. Remove very large images or split the document.") return p.name, p.read_bytes() def format_doc(manuscript, journal, style, template, guidelines, guidelines_url, want_docx, want_latex, crossref, ref_style, cite_mode, progress=gr.Progress()): if not manuscript: raise gr.Error("Upload your manuscript first.") journal, guidelines, guidelines_url = (journal or "").strip(), (guidelines or "").strip(), (guidelines_url or "").strip() if not (journal or template or guidelines or guidelines_url or _preset_key(style)): raise gr.Error("Tell the tool the target format: type the journal's name, choose a publisher style, or " "upload the journal's Word template.") if not (want_docx or want_latex): raise gr.Error("Choose at least one output: Word or LaTeX.") name, data = _data(manuscript) src, found = FormatSource(), [] progress(0.03, desc="Working out the target format") if journal: try: src.journal = lookup_journal(journal) if src.journal.get("publisher"): found.append(f"{src.journal.get('title') or journal} β€” {src.journal['publisher']}") except Exception as err: # the lookup is a convenience (Crossref), never required print(f"[formatter] journal lookup failed: {err}", flush=True) src.preset = _preset_key(style) or (src.journal or {}).get("preset") if template: try: ov, prof, notes = learn_template(*_data(template)) src.template_overrides, src.profile = ov, prof found += list(notes)[:6] except Exception as err: raise gr.Error(f"The template could not be read: {err}") text = guidelines if guidelines_url: try: text += "\n" + fetch_guidelines_url(guidelines_url) except Exception as err: found.append(f"The guidelines page could not be opened ({err}); paste its text instead.") if text.strip(): src.guidelines = parse_guidelines(text) found += list(src.guidelines.get("_found", []))[:6] manual = {} if ref_style and not ref_style.startswith("("): manual["references"] = {"style": ref_style} if cite_mode and not cite_mode.startswith("("): manual["citations"] = {"mode": cite_mode} src.manual = manual if not (src.preset or src.template_overrides or src.guidelines): raise gr.Error(f"β€œ{journal}” was not recognised as a known publisher. Choose the closest publisher style, " "or upload the journal's Word template or author guidelines.") spec = build_spec(src) try: res = format_manuscript(name, data, spec, src.profile, bool(want_docx), bool(want_latex), bool(crossref), progress=lambda f, m: progress(0.05 + 0.9 * min(1.0, f), desc=m)) except RuntimeError as err: # e.g. LaTeX input without pandoc on the server raise gr.Error(str(err)) except Exception as err: print(f"[formatter] failed on {name}: {type(err).__name__}: {err}", flush=True) raise gr.Error(f"This manuscript could not be formatted ({type(err).__name__}). If it is a PDF, try the " "Word or LaTeX source instead.") job = Path(tempfile.mkdtemp(prefix="format_", dir=str(OUT_DIR))) base = re.sub(r"[^A-Za-z0-9_-]+", "_", os.path.splitext(name)[0]).strip("_") or "manuscript" paths = {} for key, blob, fname in (("docx", res.docx, f"{base}_formatted.docx"), ("zip", res.latex_zip, f"{base}_latex.zip"), ("md", res.report_md.encode("utf-8"), f"{base}_format_report.md")): if blob: (job / fname).write_bytes(blob) paths[key] = str(job / fname) checks = res.report.checks n_ok = sum(1 for _, ok, _ in checks if ok) print(f"[formatter] {name} -> {spec.get('name', 'custom')}: {n_ok}/{len(checks)} checks, " f"{len(res.report.changes)} changes, {len(res.report.warnings)} warnings", flush=True) notes = "".join(f"
  • {html.escape(str(f))}
  • " for f in found[:8]) status = (f'
    βœ… Formatted as {html.escape(str(spec.get("name", "custom format")))}' f' β€” {n_ok} of {len(checks)} checks passed, {len(res.report.changes)} changes made, ' f'{len(res.report.warnings)} point(s) need your attention.' + (f'' if notes else "") + "
    ") buttons = download_row((paths.get("docx"), "Download Word document"), (paths.get("zip"), "Download LaTeX project"), (paths.get("md"), "Download report")) return (status, buttons, paths.get("docx"), paths.get("zip"), paths.get("md"), overleaf_html(paths.get("zip")) if paths.get("zip") else "", res.report_md) def build(ui=None): """The tab's interface (call inside `with gr.Tab(...)`). `ui` carries the suite's shared look: its animated status panel (wait, run_live, job) so this tab shows the same logo animation and progress bar as the others; without it (tests, stand-alone use) Gradio's own progress display is used.""" gr.HTML('
    Journal Formatter. Upload your manuscript and name the journal. You get ' 'it back arranged in that journal\'s format as a Word file and as a LaTeX project you can upload to ' 'Overleaf, with a report of what was changed. Equations, algorithms and tables are carried over ' 'untouched.
    1 Β· Upload the manuscript2 Β· Name the journal' '3 Β· Download Word / LaTeX for Overleaf
    ') if not AVAILABLE: gr.HTML(f'
    ⚠️ The formatter could not be loaded on this server ' f'({html.escape(WHY)}).
    ') return with gr.Row(equal_height=False): with gr.Column(elem_classes="cs-card"): ms = gr.File(label="Manuscript (.docx, .pdf, LaTeX .tex or .zip incl. an Overleaf source zip, .md)", file_types=["." + e for e in SUPPORTED], type="filepath") journal = gr.Textbox(label="Target journal or conference", placeholder="e.g. IEEE Access, Expert Systems with Applications") style = gr.Dropdown(_preset_choices(), value=AUTO, label="Publisher style", info="Leave as it is when you typed the journal's name; choose one if the journal " "is not recognised.") with gr.Accordion("πŸ“„ I have the journal's template or author guidelines (optional, most accurate)", open=False): template = gr.File(label="Journal Word template (.docx / .dotx) or a published sample paper (.pdf)", file_types=[".docx", ".dotx", ".dotm", ".doc", ".pdf"], type="filepath") guidelines = gr.Textbox(label="Author guidelines (paste the text)", lines=4) guidelines_url = gr.Textbox(label="…or the address of the guidelines page", placeholder="https://…/author-guidelines") with gr.Accordion("βš™οΈ More options (not needed)", open=False): with gr.Row(): want_docx = gr.Checkbox(value=True, label="Word (.docx)") want_latex = gr.Checkbox(value=True, label="LaTeX project for Overleaf (.zip)") crossref = gr.Checkbox(value=False, label="Check references online (slower)", info="Fills in missing volume, pages and DOI from Crossref") with gr.Row(): ref_style = gr.Dropdown(REF_STYLES, value=REF_STYLES[0], label="Reference style") cite_mode = gr.Dropdown(CITE_MODES, value=CITE_MODES[0], label="In-text citations") go = gr.Button("Format my manuscript", variant="primary", size="lg") with gr.Column(elem_classes="cs-card"): status = gr.HTML('
    Upload a manuscript, name the journal and press ' 'Format my manuscript.
    ') dl = gr.HTML() # download buttons that carry the files themselves f_docx = gr.File(label="Formatted Word document", interactive=False) f_zip = gr.File(label="LaTeX project (.zip) for Overleaf", interactive=False) overleaf = gr.HTML() f_md = gr.File(label="Formatting report", interactive=False) with gr.Accordion("Formatting report", open=False): report = gr.Markdown() inputs = [ms, journal, style, template, guidelines, guidelines_url, want_docx, want_latex, crossref, ref_style, cite_mode] outputs = [status, dl, f_docx, f_zip, f_md, overleaf, report] if ui: # the suite's branded status panel (logo animation, bar) def core(*args): # args = the inputs + the job's progress reporter return format_doc(*args[:-1], progress=args[-1]) def run(*args, request: gr.Request = None): *args, user = args # the signed-in user (or None) comes last t0, last = time.time(), None name = os.path.basename(str(args[0])) if args[0] else "" try: for frame in ui["run_live"](core, tuple(args), len(outputs), 0, user=user, request=request, tool=TOOL): last = frame yield frame except Exception as err: # shown to the customer by the status panel; log it too ui["log"](user, request, "Journal Formatter", True, {"name": name}, time.time() - t0, "error", str(getattr(err, "message", err))) raise out = last[2] or last[3] if last and len(last) > 3 else None # Word file, else the LaTeX zip ui["log"](user, request, "Journal Formatter", True, {"name": name, "output": out if isinstance(out, str) else None}, time.time() - t0, "ok") go.click(lambda: ui["wait"], None, status, queue=False, **ui["hid"]).then( run, inputs + [ui["user"]], outputs, **ui["job"](status)) else: go.click(format_doc, inputs, outputs, api_name=False, show_progress="full", concurrency_limit=int(os.getenv("FORMATTER_CONCURRENCY", "3")))