Skip to content

Commit 71d61c9

Browse files
committed
feat: add esm exports for Zotero translators (and corresponding tests)
1 parent 0b12a94 commit 71d61c9

2 files changed

Lines changed: 75 additions & 12 deletions

File tree

scripts/import_and_patch_translators.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@
3030
"DOMParser",
3131
]
3232
FW_LINE_PREFIX = "/* FW LINE 59:b820c6d */"
33+
TRANSLATOR_EXPORT_CANDIDATES = [
34+
"detectWeb",
35+
"doWeb",
36+
"detectImport",
37+
"doImport",
38+
"detectSearch",
39+
"doSearch",
40+
"doExport",
41+
]
3342

3443

3544
def should_ignore(path: Path) -> bool:
@@ -104,11 +113,41 @@ def comment_initial_json(text: str) -> tuple[str, bool]:
104113
return text, False
105114

106115

116+
def _is_function_defined(text: str, fn_name: str) -> bool:
117+
patterns = [
118+
rf"(^|\n)\s*(?:async\s+)?function\s+{re.escape(fn_name)}\s*\(",
119+
rf"(^|\n)\s*(?:var|let|const)\s+{re.escape(fn_name)}\s*=\s*(?:async\s+)?function\b",
120+
rf"(^|\n)\s*(?:var|let|const)\s+{re.escape(fn_name)}\s*=\s*(?:async\s+)?\([^)]*\)\s*=>",
121+
rf"(^|\n)\s*{re.escape(fn_name)}\s*=\s*(?:async\s+)?function\b",
122+
]
123+
return any(re.search(pattern, text) for pattern in patterns)
124+
125+
107126
def append_exports(text: str) -> tuple[str, bool]:
108-
export_snippet = "\n// Export translator functions as ES module bindings for adapter\nexport { detectWeb, doWeb };\n"
109-
if "export { detectWeb, doWeb }" in text:
127+
present = [fn for fn in TRANSLATOR_EXPORT_CANDIDATES if _is_function_defined(text, fn)]
128+
if not present:
110129
return text, False
111-
return text.rstrip() + export_snippet, True
130+
131+
export_line = f"export {{ {', '.join(present)} }};"
132+
export_snippet = (
133+
"\n// Export translator functions as ES module bindings for adapter\n"
134+
+ export_line
135+
+ "\n"
136+
)
137+
138+
# Remove a previously generated trailing export block, so reruns stay idempotent
139+
generated_block_re = re.compile(
140+
r"\n?// Export translator functions as ES module bindings for adapter\n"
141+
r"export\s*\{[^}]*\};\s*\Z",
142+
re.MULTILINE,
143+
)
144+
new_text = generated_block_re.sub("", text).rstrip()
145+
146+
# If an identical export already exists, avoid changing file layout
147+
if re.search(rf"(^|\n)\s*{re.escape(export_line)}\s*(\n|\Z)", new_text):
148+
return new_text + "\n", new_text != text
149+
150+
return new_text + export_snippet, True
112151

113152

114153
def ensure_sandbox_import(text: str) -> tuple[str, bool]:
@@ -200,23 +239,25 @@ def extract_json_from_text(text: str):
200239
return None
201240

202241

203-
def process_file(path: Path) -> tuple[bool, bool, bool]:
242+
def process_file(path: Path) -> tuple[bool, bool, bool, bool]:
204243
commented = False
205244
imported = False
245+
exported = False
206246

207247
text = path.read_text(encoding="utf-8")
208248

209249
# Delete old translators that still use the deprecated "Zotero Framework"
210250
# These are not valid esm, and are deprecated anyway: https://github.com/zotero/translators/issues/3105
211251
if has_fw_line(text):
212252
path.unlink()
213-
return commented, imported, True
253+
return commented, imported, exported, True
214254

215255
text, commented = comment_initial_json(text)
216256

217257
text, imported = ensure_sandbox_import(text)
258+
text, exported = append_exports(text)
218259

219-
if commented or imported:
260+
if commented or imported or exported:
220261
# backup original
221262
# bak = path.with_suffix(path.suffix + '.bak')
222263
# try:
@@ -226,7 +267,7 @@ def process_file(path: Path) -> tuple[bool, bool, bool]:
226267
# pass
227268
path.write_text(text, encoding="utf-8")
228269

229-
return commented, imported, False
270+
return commented, imported, exported, False
230271

231272

232273
def patch_all():
@@ -238,22 +279,25 @@ def patch_all():
238279
total = 0
239280
commented_count = 0
240281
imported_count = 0
282+
exported_count = 0
241283
deleted_count = 0
242284
for f in js_files:
243285
total += 1
244286
try:
245-
commented, imported, deleted = process_file(f)
287+
commented, imported, exported, deleted = process_file(f)
246288
if commented:
247289
commented_count += 1
248290
if imported:
249291
imported_count += 1
292+
if exported:
293+
exported_count += 1
250294
if deleted:
251295
deleted_count += 1
252296
except Exception as e:
253297
print("Error processing", f, e)
254298

255299
print(
256-
f"Processed {total} files: deleted {deleted_count}, commented {commented_count}, sandbox imports updated {imported_count}"
300+
f"Processed {total} files: deleted {deleted_count}, commented {commented_count}, sandbox imports updated {imported_count}, exports updated {exported_count}"
257301
)
258302

259303

tests/translator-import.test.mjs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import { setSandbox } from "../sources/sandbox.js";
77

88
const TRANSLATORS_DIR = path.resolve(process.cwd(), "translators", "zotero");
99
const translatorFiles = (await readdir(TRANSLATORS_DIR)).filter((name) => name.endsWith(".js"));
10+
const EXPORT_CANDIDATES = [
11+
"detectWeb",
12+
"doWeb",
13+
"detectImport",
14+
"doImport",
15+
"detectSearch",
16+
"doSearch",
17+
"doExport",
18+
];
1019

1120
setSandbox({
1221
ZU: {
@@ -29,11 +38,21 @@ setSandbox({
2938

3039
describe("Zotero translator", () => {
3140
for (const filename of translatorFiles) {
41+
const filePath = path.join(TRANSLATORS_DIR, filename);
42+
const moduleUrl = pathToFileURL(filePath).href;
3243
it(`imports ${filename}`, async () => {
33-
const filePath = path.join(TRANSLATORS_DIR, filename);
34-
const moduleUrl = pathToFileURL(filePath).href;
35-
3644
await expect(import(moduleUrl)).resolves.toBeDefined();
3745
});
46+
47+
it(`exports expected entry points for ${filename}`, async () => {
48+
const mod = await import(moduleUrl);
49+
const exportedEntryPoints = EXPORT_CANDIDATES.filter(
50+
(fnName) => typeof mod[fnName] === "function",
51+
);
52+
53+
expect(exportedEntryPoints, `${filename} should export at least one entry point`).not.toBe(
54+
[],
55+
);
56+
});
3857
}
3958
});

0 commit comments

Comments
 (0)