unknown
Claude Opus 5.5
Journal Formatter and Journal Finder as native tabs of the suite (work in progress)
7ee2a84
Raw History Blame Contribute Delete
32 kB
"""Format specification: defaults, publisher presets, merging, journal lookup."""
from __future__ import annotations
import copy
import difflib
import json
import re
from typing import Optional
# ---------------------------------------------------------------- defaults
DEFAULT_SPEC = {
"name": "Generic",
"page": {"size": "A4", "margins_cm": [2.54, 2.54, 2.54, 2.54], # top, bottom, left, right
"columns": 1, "col_gap_cm": 0.5, "front_matter_single_column": True,
"page_numbers": True, "line_numbers": False},
"font": {"family": "Times New Roman", "size": 12, "line_spacing": 1.5, "align": "justify",
"first_line_indent_cm": 0.0, "space_before_pt": 0, "space_after_pt": 6},
"title": {"size": 16, "bold": True, "italic": False, "align": "center", "case": "as-is", "space_after_pt": 12},
"authors": {"size": 11, "bold": False, "italic": False, "align": "center"},
"affiliation": {"size": 10, "bold": False, "italic": True, "align": "center"},
"abstract": {"heading": "Abstract", "layout": "block", # block | inline
"suffix": ": ", "size": 11, "label_bold": True, "label_italic": False,
"body_bold": False, "body_italic": False, "align": "justify",
"structured": False,
"structured_labels": ["Purpose", "Design/methodology/approach", "Findings",
"Originality/value"],
"indent_cm": 0.0},
"keywords": {"label": "Keywords", "suffix": ": ", "separator": "; ", "size": 11,
"label_bold": True, "label_italic": False, "body_italic": False, "case": "as-is",
"end_period": False},
"headings": {
"scheme": "decimal", # decimal | ieee | none
"decimal_trailing_dot": True, # "1. Introduction" vs "1 Introduction"
"subsection_trailing_dot": True, # "1.1. Data" vs "1.1 Data"
"levels": [
{"size": 12, "bold": True, "italic": False, "case": "as-is", "align": "left", "smallcaps": False,
"space_before_pt": 12, "space_after_pt": 6},
{"size": 12, "bold": True, "italic": False, "case": "as-is", "align": "left", "smallcaps": False,
"space_before_pt": 10, "space_after_pt": 4},
{"size": 12, "bold": True, "italic": True, "case": "as-is", "align": "left", "smallcaps": False,
"space_before_pt": 8, "space_after_pt": 4},
{"size": 12, "bold": False, "italic": True, "case": "as-is", "align": "left", "smallcaps": False,
"space_before_pt": 6, "space_after_pt": 2},
],
},
"captions": {"figure_label": "Figure", "table_label": "Table", "table_numbering": "arabic",
"table_label_case": "as-is", "label_bold": True, "label_italic": False,
"separator": ". ", "size": 10, "align": "center", "table_position": "above",
"table_label_newline": False, "body_italic": False, "end_period": True},
"tables": {"size": 10, "borders": "booktabs", "header_bold": True},
"references": {"style": "apa", "heading": "References", "size": 11, "hanging_indent_cm": 1.27},
"citations": {"mode": "author-year"}, # numeric | superscript | author-year
"body_xref": {"figure": "Figure", "table": "Table"},
"equations": {"align": "center"},
"latex": {"class": "article", "options": "11pt,a4paper", "bibstyle": "", "packages": []},
"limits": {"abstract_words": None, "keywords_min": None, "keywords_max": None,
"title_words": None, "total_words": None},
"required_sections": [],
}
def deep_merge(base: dict, over: dict) -> dict:
out = copy.deepcopy(base)
for k, v in (over or {}).items():
if v is None:
continue
if isinstance(v, dict) and isinstance(out.get(k), dict):
out[k] = deep_merge(out[k], v)
elif k == "levels" and isinstance(v, list) and isinstance(out.get(k), list):
merged = []
for i in range(max(len(v), len(out[k]))):
b = out[k][i] if i < len(out[k]) else {}
o = v[i] if i < len(v) else {}
merged.append(deep_merge(b, o or {}))
out[k] = merged
else:
out[k] = copy.deepcopy(v)
return out
def _lv(**kw):
return kw
# ---------------------------------------------------------------- presets
PRESETS = {
"ieee_journal": {
"name": "IEEE Transactions / Journals (incl. IEEE Access)",
"page": {"size": "Letter", "margins_cm": [1.9, 2.54, 1.57, 1.57], "columns": 2, "col_gap_cm": 0.43},
"font": {"family": "Times New Roman", "size": 10, "line_spacing": 1.0, "first_line_indent_cm": 0.36,
"space_after_pt": 0},
"title": {"size": 24, "bold": False, "case": "title"},
"authors": {"size": 11},
"affiliation": {"size": 8, "italic": False},
"abstract": {"heading": "Abstract", "layout": "inline", "suffix": "—", "size": 9, "label_bold": True,
"label_italic": True, "body_bold": True},
"keywords": {"label": "Index Terms", "suffix": "—", "separator": ", ", "size": 9, "label_bold": True,
"label_italic": True, "body_italic": False, "case": "lower-first", "end_period": True},
"headings": {"scheme": "ieee", "levels": [
_lv(size=10, bold=False, italic=False, case="title", align="center", smallcaps=True),
_lv(size=10, bold=False, italic=True, case="title", align="left"),
_lv(size=10, bold=False, italic=True, case="sentence", align="left"),
_lv(size=10, bold=False, italic=True, case="sentence", align="left")]},
"captions": {"figure_label": "Fig.", "table_label": "Table", "table_numbering": "roman",
"table_label_case": "upper", "label_bold": False, "separator": ". ", "size": 8,
"table_label_newline": True, "table_caption_case": "upper", "table_end_period": False},
"tables": {"size": 8},
"references": {"style": "ieee", "heading": "References", "size": 8, "hanging_indent_cm": 0.63},
"citations": {"mode": "numeric"},
"body_xref": {"figure": "Fig.", "table": "Table"},
"latex": {"class": "IEEEtran", "options": "journal", "bibstyle": "IEEEtran"},
"limits": {"abstract_words": 250, "keywords_min": 3, "keywords_max": 10},
},
"ieee_conference": {
"name": "IEEE Conference",
"page": {"size": "Letter", "margins_cm": [1.9, 2.54, 1.78, 1.78], "columns": 2, "col_gap_cm": 0.64},
"font": {"family": "Times New Roman", "size": 10, "line_spacing": 1.0, "first_line_indent_cm": 0.36,
"space_after_pt": 0},
"title": {"size": 24, "bold": False, "case": "title"},
"authors": {"size": 11},
"affiliation": {"size": 10, "italic": True},
"abstract": {"heading": "Abstract", "layout": "inline", "suffix": "—", "size": 9, "label_bold": True,
"label_italic": True, "body_bold": True},
"keywords": {"label": "Index Terms", "suffix": "—", "separator": ", ", "size": 9, "label_bold": True,
"label_italic": True, "body_bold": True, "case": "lower-first", "end_period": False},
"headings": {"scheme": "ieee", "levels": [
_lv(size=10, bold=False, italic=False, case="title", align="center", smallcaps=True),
_lv(size=10, bold=False, italic=True, case="title", align="left"),
_lv(size=10, bold=False, italic=True, case="sentence", align="left"),
_lv(size=10, bold=False, italic=True, case="sentence", align="left")]},
"captions": {"figure_label": "Fig.", "table_label": "Table", "table_numbering": "roman",
"table_label_case": "upper", "label_bold": False, "separator": ". ", "size": 8,
"table_label_newline": True, "table_caption_case": "upper", "table_end_period": False},
"tables": {"size": 8},
"references": {"style": "ieee", "heading": "References", "size": 8, "hanging_indent_cm": 0.63},
"citations": {"mode": "numeric"},
"body_xref": {"figure": "Fig.", "table": "Table"},
"latex": {"class": "IEEEtran", "options": "conference", "bibstyle": "IEEEtran"},
"limits": {"abstract_words": 200},
},
"elsevier": {
"name": "Elsevier (elsarticle, numbered references)",
"page": {"size": "A4", "margins_cm": [2.5, 2.5, 2.5, 2.5], "columns": 1, "line_numbers": True},
"font": {"family": "Times New Roman", "size": 12, "line_spacing": 2.0},
"title": {"size": 17, "bold": False, "case": "as-is"},
"abstract": {"heading": "Abstract", "layout": "block", "size": 11},
"keywords": {"label": "Keywords", "suffix": ": ", "separator": "; ", "size": 11},
"headings": {"scheme": "decimal", "decimal_trailing_dot": True, "subsection_trailing_dot": True,
"levels": [_lv(size=12, bold=True), _lv(size=12, bold=False, italic=True),
_lv(size=12, bold=False, italic=True), _lv(size=12, bold=False, italic=True)]},
"captions": {"figure_label": "Fig.", "table_label": "Table", "label_bold": True, "separator": ". ",
"size": 10},
"references": {"style": "elsevier-numeric", "size": 11, "hanging_indent_cm": 0.75},
"citations": {"mode": "numeric"},
"body_xref": {"figure": "Fig.", "table": "Table"},
"latex": {"class": "elsarticle", "options": "preprint,12pt", "bibstyle": "elsarticle-num"},
"limits": {"abstract_words": 250, "keywords_min": 1, "keywords_max": 7},
},
"elsevier_harvard": {
"name": "Elsevier (author–year / Harvard)",
"_base": "elsevier",
"references": {"style": "harvard"},
"citations": {"mode": "author-year"},
"latex": {"bibstyle": "elsarticle-harv"},
},
"springer_journal": {
"name": "Springer Nature journals (sn-jnl)",
"page": {"size": "A4", "margins_cm": [2.5, 2.5, 2.5, 2.5], "columns": 1},
"font": {"family": "Times New Roman", "size": 11, "line_spacing": 1.5},
"title": {"size": 16, "bold": True},
"abstract": {"heading": "Abstract", "layout": "block", "size": 10},
"keywords": {"label": "Keywords", "suffix": " ", "separator": " · ", "size": 10},
"headings": {"scheme": "decimal", "decimal_trailing_dot": False, "subsection_trailing_dot": False,
"levels": [_lv(size=12, bold=True), _lv(size=11, bold=True),
_lv(size=11, bold=False, italic=True), _lv(size=11, italic=True, bold=False)]},
"captions": {"figure_label": "Fig.", "table_label": "Table", "label_bold": True, "separator": " ",
"size": 9, "end_period": False},
"references": {"style": "springer-basic", "size": 10},
"citations": {"mode": "author-year-nocomma"},
"body_xref": {"figure": "Fig.", "table": "Table"},
"latex": {"class": "article", "options": "11pt,a4paper", "bibstyle": "spbasic",
"note": "Springer's sn-jnl class is not installed on Overleaf by default; this export uses article "
"with Springer layout. To switch, add sn-jnl.cls from the Springer Nature LaTeX template."},
"limits": {"abstract_words": 250, "keywords_min": 4, "keywords_max": 6},
},
"springer_numeric": {
"name": "Springer Nature journals (numbered references)",
"_base": "springer_journal",
"references": {"style": "springer-vancouver"},
"citations": {"mode": "numeric"},
"latex": {"bibstyle": "spmpsci"},
},
"springer_lncs": {
"name": "Springer LNCS / conference proceedings",
"page": {"size": "A4", "margins_cm": [5.25, 5.25, 4.4, 4.4], "columns": 1},
"font": {"family": "Times New Roman", "size": 10, "line_spacing": 1.0, "first_line_indent_cm": 0.42,
"space_after_pt": 0},
"title": {"size": 14, "bold": True},
"authors": {"size": 10},
"affiliation": {"size": 9, "italic": False},
"abstract": {"heading": "Abstract", "layout": "inline", "suffix": ". ", "size": 9, "label_bold": True},
"keywords": {"label": "Keywords", "suffix": ": ", "separator": " · ", "size": 9, "label_bold": True},
"headings": {"scheme": "decimal", "decimal_trailing_dot": False, "subsection_trailing_dot": False,
"levels": [_lv(size=12, bold=True), _lv(size=10, bold=True),
_lv(size=10, bold=True), _lv(size=10, italic=True, bold=False)]},
"captions": {"figure_label": "Fig.", "table_label": "Table", "label_bold": True, "separator": ". ",
"size": 9},
"tables": {"size": 9},
"references": {"style": "lncs", "size": 9, "hanging_indent_cm": 0.5},
"citations": {"mode": "numeric"},
"body_xref": {"figure": "Fig.", "table": "Table"},
"latex": {"class": "llncs", "options": "runningheads", "bibstyle": "splncs04"},
"limits": {"abstract_words": 250, "keywords_min": 3, "keywords_max": 6},
},
"mdpi": {
"name": "MDPI journals",
"page": {"size": "A4", "margins_cm": [2.5, 2.5, 2.0, 2.0], "columns": 1},
"font": {"family": "Palatino Linotype", "size": 10, "line_spacing": 1.0, "first_line_indent_cm": 0.75,
"space_after_pt": 0},
"title": {"size": 18, "bold": True, "align": "left"},
"authors": {"size": 10, "bold": True, "align": "left"},
"affiliation": {"size": 8, "italic": False, "align": "left"},
"abstract": {"heading": "Abstract", "layout": "inline", "suffix": ": ", "size": 9, "label_bold": True},
"keywords": {"label": "Keywords", "suffix": ": ", "separator": "; ", "size": 9, "label_bold": True},
"headings": {"scheme": "decimal", "decimal_trailing_dot": True, "subsection_trailing_dot": True,
"levels": [_lv(size=10, bold=True), _lv(size=10, bold=False, italic=True),
_lv(size=10, bold=False, italic=False), _lv(size=10, italic=True, bold=False)]},
"captions": {"figure_label": "Figure", "table_label": "Table", "label_bold": True, "separator": ". ",
"size": 9, "align": "left"},
"tables": {"size": 9},
"references": {"style": "mdpi", "size": 9, "hanging_indent_cm": 0.6},
"citations": {"mode": "numeric"},
"latex": {"class": "article", "options": "10pt,a4paper", "bibstyle": "mdpi"},
"limits": {"abstract_words": 200, "keywords_min": 3, "keywords_max": 10},
},
"emerald": {
"name": "Emerald journals (structured abstract, Harvard)",
"page": {"size": "A4", "margins_cm": [2.54, 2.54, 2.54, 2.54], "columns": 1},
"font": {"family": "Times New Roman", "size": 12, "line_spacing": 2.0},
"abstract": {"heading": "Abstract", "layout": "block", "structured": True, "size": 12},
"keywords": {"label": "Keywords", "suffix": " ", "separator": ", ", "size": 12, "case": "title"},
"headings": {"scheme": "decimal", "decimal_trailing_dot": True, "subsection_trailing_dot": False,
"levels": [_lv(size=12, bold=True), _lv(size=12, bold=True, italic=True),
_lv(size=12, bold=False, italic=True), _lv(size=12, italic=True, bold=False)]},
"captions": {"figure_label": "Figure", "table_label": "Table", "label_bold": True, "separator": ". ",
"size": 11, "end_period": False},
"references": {"style": "emerald-harvard", "size": 12},
"citations": {"mode": "author-year"},
"latex": {"class": "article", "options": "12pt,a4paper"},
"limits": {"abstract_words": 250, "keywords_max": 12, "title_words": 15},
},
"taylor_francis": {
"name": "Taylor & Francis (APA style)",
"page": {"size": "A4", "margins_cm": [2.54, 2.54, 2.54, 2.54], "columns": 1},
"font": {"family": "Times New Roman", "size": 12, "line_spacing": 2.0},
"abstract": {"heading": "Abstract", "layout": "block", "size": 12},
"keywords": {"label": "Keywords", "suffix": ": ", "separator": "; ", "size": 12, "label_italic": False},
"headings": {"scheme": "decimal", "decimal_trailing_dot": True, "subsection_trailing_dot": False,
"levels": [_lv(size=12, bold=True), _lv(size=12, bold=True, italic=True),
_lv(size=12, bold=False, italic=True), _lv(size=12, italic=True, bold=False)]},
"captions": {"figure_label": "Figure", "table_label": "Table", "label_bold": False, "separator": ". ",
"size": 11},
"references": {"style": "apa", "size": 12},
"citations": {"mode": "author-year"},
"latex": {"class": "article", "options": "12pt,a4paper"},
"limits": {"abstract_words": 200, "keywords_min": 3, "keywords_max": 6},
},
"wiley": {
"name": "Wiley journals (generic, APA)",
"_base": "taylor_francis",
"references": {"style": "apa"},
},
"apa7": {
"name": "APA 7th edition (theses, APA journals)",
"page": {"size": "Letter", "margins_cm": [2.54, 2.54, 2.54, 2.54], "columns": 1},
"font": {"family": "Times New Roman", "size": 12, "line_spacing": 2.0, "first_line_indent_cm": 1.27,
"space_after_pt": 0, "align": "left"},
"title": {"size": 12, "bold": True, "case": "title"},
"authors": {"size": 12}, "affiliation": {"size": 12, "italic": False},
"abstract": {"heading": "Abstract", "layout": "block", "size": 12, "align": "left"},
"keywords": {"label": "Keywords", "suffix": ": ", "separator": ", ", "size": 12, "label_italic": True,
"label_bold": False},
"headings": {"scheme": "none", "levels": [
_lv(size=12, bold=True, case="title", align="center"),
_lv(size=12, bold=True, case="title", align="left"),
_lv(size=12, bold=True, italic=True, case="title", align="left"),
_lv(size=12, bold=True, case="title", align="left")]},
"captions": {"figure_label": "Figure", "table_label": "Table", "label_bold": True, "separator": "\n",
"size": 12, "align": "left", "body_italic": True, "end_period": False,
"figure_position": "above"},
"references": {"style": "apa", "size": 12, "hanging_indent_cm": 1.27},
"citations": {"mode": "author-year"},
"latex": {"class": "article", "options": "12pt,letterpaper"},
"limits": {"abstract_words": 250, "keywords_min": 3, "keywords_max": 5},
},
"plos": {
"name": "PLOS (Vancouver)",
"page": {"size": "Letter", "columns": 1, "line_numbers": True},
"font": {"family": "Arial", "size": 10, "line_spacing": 1.5, "align": "left"},
"title": {"size": 18, "bold": True, "align": "left"},
"abstract": {"heading": "Abstract", "layout": "block", "size": 10},
"headings": {"scheme": "none", "levels": [_lv(size=14, bold=True), _lv(size=12, bold=True),
_lv(size=10, bold=True), _lv(size=10, italic=True, bold=False)]},
"captions": {"figure_label": "Fig", "table_label": "Table", "separator": ". ", "size": 10, "align": "left"},
"references": {"style": "vancouver", "size": 10, "hanging_indent_cm": 0.63},
"citations": {"mode": "numeric"},
"body_xref": {"figure": "Fig", "table": "Table"},
"latex": {"class": "article", "options": "10pt,letterpaper"},
"limits": {"abstract_words": 300},
},
"frontiers": {
"name": "Frontiers journals",
"page": {"size": "A4", "columns": 1},
"font": {"family": "Times New Roman", "size": 12, "line_spacing": 1.15},
"abstract": {"heading": "Abstract", "layout": "block", "size": 12},
"keywords": {"label": "Keywords", "suffix": ": ", "separator": ", "},
"headings": {"scheme": "decimal", "decimal_trailing_dot": False, "subsection_trailing_dot": False,
"levels": [_lv(bold=True, case="upper"), _lv(bold=True), _lv(bold=True, italic=True),
_lv(italic=True, bold=False)]},
"captions": {"figure_label": "Figure", "table_label": "Table", "label_bold": True, "separator": " "},
"references": {"style": "harvard", "size": 12},
"citations": {"mode": "author-year"},
"limits": {"abstract_words": 350, "keywords_min": 5, "keywords_max": 8},
},
"generic_numeric": {
"name": "Generic numbered (Vancouver)",
"references": {"style": "vancouver"}, "citations": {"mode": "numeric"},
},
}
def get_preset(key: str) -> dict:
p = copy.deepcopy(PRESETS[key])
base = p.pop("_base", None)
spec = get_preset(base) if base else copy.deepcopy(DEFAULT_SPEC)
return deep_merge(spec, p)
# ---------------------------------------------------------------- journal name → preset
PUBLISHER_MAP = [
(r"\bieee\b|institute of electrical", "ieee_journal"),
(r"elsevier|cell press|academic press", "elsevier"),
(r"lecture notes|lncs|ccis", "springer_lncs"),
(r"springer|nature|bmc|biomed central|palgrave", "springer_numeric"),
(r"mdpi|multidisciplinary digital", "mdpi"),
(r"emerald", "emerald"),
(r"taylor|francis|informa|routledge", "taylor_francis"),
(r"wiley|hindawi|blackwell", "wiley"),
(r"public library of science|\bplos\b", "plos"),
(r"frontiers", "frontiers"),
(r"american psychological|\bapa\b", "apa7"),
]
KNOWN_JOURNALS = {
"ieee access": "ieee_journal", "international journal of organizational analysis": "emerald",
"expert systems with applications": "elsevier", "computers & security": "elsevier",
"computers and security": "elsevier", "future generation computer systems": "elsevier",
"applied energy": "elsevier", "journal of energy storage": "elsevier",
"scientific reports": "springer_numeric", "sensors": "mdpi", "electronics": "mdpi",
"sustainability": "mdpi", "applied sciences": "mdpi", "energies": "mdpi",
"plos one": "plos", "management decision": "emerald", "journal of knowledge management": "emerald",
"international journal of intelligent engineering and systems": "ieee_journal",
}
def preset_for_publisher(text: str) -> Optional[str]:
t = (text or "").lower()
for pat, key in PUBLISHER_MAP:
if re.search(pat, t):
if key == "ieee_journal" and re.search(r"conference|proceedings|symposium|workshop", t):
return "ieee_conference"
return key
return None
def lookup_journal(name: str, timeout: float = 8.0) -> dict:
"""Resolve a journal name to {title, publisher, issn, preset, source}. Network optional."""
n = (name or "").strip()
res = {"query": n, "title": n, "publisher": "", "issn": "", "preset": None, "source": ""}
if not n:
return res
key = n.lower()
if key in KNOWN_JOURNALS:
res.update(preset=KNOWN_JOURNALS[key], source="built-in list")
direct = preset_for_publisher(n)
if not res["preset"] and direct:
res.update(preset=direct, source="journal name")
try:
import requests
r = requests.get("https://api.crossref.org/journals", params={"query": n, "rows": 8},
headers={"User-Agent": "FormatMatch/1.0 (mailto:info@cssoftwaresolutions.tech)"},
timeout=timeout)
items = r.json()["message"]["items"]
if items:
best = max(items, key=lambda it: difflib.SequenceMatcher(None, it.get("title", "").lower(), key).ratio())
score = difflib.SequenceMatcher(None, best.get("title", "").lower(), key).ratio()
if score > 0.6:
res["title"] = best.get("title", n)
res["publisher"] = best.get("publisher", "")
res["issn"] = ", ".join(best.get("ISSN", []))
pp = preset_for_publisher(res["publisher"] + " " + res["title"])
if pp and not res["preset"]:
res.update(preset=pp, source="Crossref publisher")
except Exception:
pass
return res
# ---------------------------------------------------------------- guidelines text → overrides
FONT_NAMES = ["Times New Roman", "Arial", "Calibri", "Cambria", "Palatino Linotype", "Palatino", "Garamond",
"Helvetica", "Georgia", "Book Antiqua", "Verdana", "Computer Modern"]
REF_STYLE_WORDS = [
(r"\bieee\b(?:\s+(?:reference|citation|style))", "ieee"), (r"\bapa\b", "apa"), (r"harvard", "harvard"),
(r"vancouver", "vancouver"), (r"chicago", "chicago"), (r"\bama\b|american medical association", "ama"),
(r"\bmla\b", "mla"), (r"springer basic|sn-basic", "springer-basic"),
]
NUM_WORDS = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8,
"nine": 9, "ten": 10, "twelve": 12, "fifteen": 15}
def _num(s):
s = s.lower()
return NUM_WORDS.get(s) or (float(s) if re.fullmatch(r"\d+(?:\.\d+)?", s) else None)
def parse_guidelines(text: str) -> dict:
"""Rule-based extraction of formatting requirements from author-guidelines text."""
t = re.sub(r"\s+", " ", text or "")
tl = t.lower()
o: dict = {"page": {}, "font": {}, "abstract": {}, "keywords": {}, "references": {}, "citations": {},
"limits": {}, "headings": {}}
found = []
for f in FONT_NAMES:
if f.lower() in tl:
o["font"]["family"] = "Palatino Linotype" if f == "Palatino" else f
found.append(f"font: {f}")
break
m = re.search(r"(\d{1,2}(?:\.\d)?)\s*-?\s*(?:pt|point)\b", tl)
if m:
o["font"]["size"] = float(m.group(1))
found.append(f"font size: {m.group(1)} pt")
if re.search(r"double[- ]spac", tl):
o["font"]["line_spacing"] = 2.0; found.append("double spacing")
elif re.search(r"1\.5[- ]?(?:line)?[- ]?spac|one and a half", tl):
o["font"]["line_spacing"] = 1.5; found.append("1.5 spacing")
elif re.search(r"single[- ]spac", tl):
o["font"]["line_spacing"] = 1.0; found.append("single spacing")
m = re.search(r"margins?[^.]{0,40}?(\d(?:\.\d+)?)\s*(cm|mm|inch|inches|in\b|\")", tl)
if m:
v = float(m.group(1)); u = m.group(2)
cm = v * 2.54 if u.startswith("in") or u == '"' else (v / 10 if u == "mm" else v)
o["page"]["margins_cm"] = [round(cm, 2)] * 4; found.append(f"margins: {v}{u}")
if re.search(r"\ba4\b", tl):
o["page"]["size"] = "A4"; found.append("A4 paper")
elif re.search(r"\bus letter\b|letter[- ]size|8\.5\s*[x×]\s*11", tl):
o["page"]["size"] = "Letter"; found.append("Letter paper")
if re.search(r"two[- ]column|double[- ]column", tl):
o["page"]["columns"] = 2; found.append("two columns")
elif re.search(r"single[- ]column|one[- ]column", tl):
o["page"]["columns"] = 1; found.append("single column")
if re.search(r"line number", tl):
o["page"]["line_numbers"] = True; found.append("line numbers")
m = re.search(r"abstract[^.]{0,80}?(?:not exceed|maximum of|max(?:imum)?\.?|no more than|up to|within|limited to|of about|of)\s*(\d{2,3})\s*words", tl) \
or re.search(r"abstract[^.]{0,30}?\(?(\d{2,3})\s*words", tl) \
or re.search(r"(\d{2,3})[- ]word abstract", tl)
if m:
o["limits"]["abstract_words"] = int(m.group(1)); found.append(f"abstract ≤ {m.group(1)} words")
if re.search(r"structured abstract", tl) or re.search(r"purpose.{0,40}design/methodology/approach", tl):
o["abstract"]["structured"] = True; found.append("structured abstract")
m = re.search(r"(\w+)\s*(?:to|-|–)\s*(\w+)\s*key\s*words", tl)
if m and _num(m.group(1)) and _num(m.group(2)):
o["limits"]["keywords_min"] = int(_num(m.group(1))); o["limits"]["keywords_max"] = int(_num(m.group(2)))
found.append(f"{m.group(1)}–{m.group(2)} keywords")
else:
m = re.search(r"(?:up to|maximum of|max\.?|no more than)\s*(\w+)\s*key\s*words", tl)
if m and _num(m.group(1)):
o["limits"]["keywords_max"] = int(_num(m.group(1))); found.append(f"≤ {m.group(1)} keywords")
m = re.search(r"title[^.]{0,40}?(?:not exceed|maximum of|no more than|up to)\s*(\d{1,3})\s*words", tl)
if m:
o["limits"]["title_words"] = int(m.group(1)); found.append(f"title ≤ {m.group(1)} words")
m = re.search(r"(?:manuscript|paper|article|text)[^.]{0,60}?(?:not exceed|maximum of|no more than|up to|within)\s*([\d,]{4,6})\s*words", tl)
if m:
o["limits"]["total_words"] = int(m.group(1).replace(",", "")); found.append(f"≤ {m.group(1)} words total")
for pat, style in REF_STYLE_WORDS:
if re.search(pat, tl):
o["references"]["style"] = style
o["citations"]["mode"] = "numeric" if style in ("ieee", "vancouver", "ama") else "author-year"
found.append(f"reference style: {style}")
break
if "style" not in o["references"] and re.search(r"numbered (?:consecutively )?in (?:the )?order|square brackets", tl):
o["citations"]["mode"] = "numeric"; o["references"].setdefault("style", "vancouver")
found.append("numbered citations")
if re.search(r"(?:headings?|sections?)[^.]{0,40}(?:should not|must not|not) be numbered|unnumbered (?:sections|headings)", tl):
o["headings"]["scheme"] = "none"; found.append("unnumbered headings")
elif re.search(r"(?:headings?|sections?)[^.]{0,40}(?:should|must) be numbered|numbered (?:sections|headings)", tl):
o["headings"]["scheme"] = "decimal"; found.append("numbered headings")
req = []
for sec in ["Introduction", "Literature Review", "Methodology", "Methods", "Results", "Discussion",
"Conclusion", "Acknowledgements", "Data Availability", "Conflict of Interest", "Funding",
"Author Contributions", "Declaration of Competing Interest", "Ethics Statement"]:
if re.search(r"(?:must|should|required|include|mandatory)[^.]{0,120}\b" + sec.lower(), tl):
req.append(sec)
if req:
o["required_sections"] = req; found.append("required sections: " + ", ".join(req))
o = {k: v for k, v in o.items() if v}
o["_found"] = found
return o
def parse_guidelines_llm(text: str, api_key: str, model: str = "llama-3.3-70b-versatile") -> dict:
"""Optional: ask a Groq-hosted LLM to map guidelines to spec overrides (JSON)."""
import requests
schema_hint = json.dumps({k: DEFAULT_SPEC[k] for k in
["page", "font", "title", "abstract", "keywords", "headings", "captions",
"references", "citations", "limits", "required_sections"]})
prompt = ("Extract manuscript formatting rules from the author guidelines below. Return ONLY a JSON object "
"with the same keys/structure as this schema, filling only values the guidelines state "
"(omit unknowns). references.style must be one of: ieee, apa, harvard, emerald-harvard, vancouver, "
"chicago, ama, mla, springer-basic, springer-vancouver, lncs, mdpi, elsevier-numeric. "
"citations.mode one of numeric, superscript, author-year. headings.scheme one of decimal, ieee, none."
f"\n\nSCHEMA:\n{schema_hint}\n\nGUIDELINES:\n{text[:24000]}")
r = requests.post("https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": f"Bearer {api_key}"},
json={"model": model, "temperature": 0, "response_format": {"type": "json_object"},
"messages": [{"role": "user", "content": prompt}]}, timeout=90)
r.raise_for_status()
data = json.loads(r.json()["choices"][0]["message"]["content"])
data["_found"] = ["LLM-extracted rules (review below)"]
return data