@@ -97,39 +97,137 @@ def _is_function_defined(text: str, fn_name: str) -> bool:
9797 return any (re .search (pattern , text ) for pattern in patterns )
9898
9999
100- def append_exports (text : str ) -> tuple [str , bool ]:
101- present = [fn for fn in TRANSLATOR_EXPORT_CANDIDATES if _is_function_defined (text , fn )]
102- if not present :
103- return text , False
100+ def _parse_generated_export_specs (specs_text : str ) -> list [str ]:
101+ return [spec .strip () for spec in specs_text .split ("," ) if spec .strip ()]
104102
105- export_line = f"export {{ { ', ' .join (present )} }};"
106- export_snippet = (
107- "\n // Export translator functions as ES module bindings for adapter\n "
108- + export_line
109- + "\n "
110- )
111103
112- # Remove a previously generated metadata+functions trailing block
113- generated_metadata_block_re = re .compile (
114- r"\n?// Export translator metadata for sandbox adapter\n"
115- r"export\s+const\s+ZOTERO_TRANSLATOR_INFO\s*=\s*[^\n]*;\n"
116- r"(?:// Export translator functions as ES module bindings for adapter\n"
117- r"export\s*\{[^}]*\};\n)?\s*\Z" ,
104+ def _build_exports_body_from_specs (specs : list [str ]) -> str :
105+ entries = []
106+ for spec in specs :
107+ if " as " in spec :
108+ local_name , export_name = [part .strip () for part in spec .split (" as " , 1 )]
109+ entries .append (f"{ export_name } : { local_name } " )
110+ else :
111+ entries .append (spec )
112+ if not entries :
113+ return ""
114+ return " " + ", " .join (entries ) + " "
115+
116+
117+ def _extract_and_remove_exports_object (text : str ) -> tuple [str , str | None , bool ]:
118+ m = re .search (r"(?:export\s+)?(?:var|let|const)\s+exports\s*=\s*\{" , text )
119+ if not m :
120+ return text , None , False
121+
122+ declaration_start = m .start ()
123+ open_brace_index = text .find ("{" , declaration_start )
124+ if open_brace_index == - 1 :
125+ return text , None , False
126+
127+ i = open_brace_index
128+ depth = 0
129+ in_str = None
130+ esc = False
131+ close_brace_index = None
132+ while i < len (text ):
133+ ch = text [i ]
134+ if in_str :
135+ if esc :
136+ esc = False
137+ elif ch == "\\ " :
138+ esc = True
139+ elif ch == in_str :
140+ in_str = None
141+ else :
142+ if ch == '"' or ch == "'" :
143+ in_str = ch
144+ elif ch == "{" :
145+ depth += 1
146+ elif ch == "}" :
147+ depth -= 1
148+ if depth == 0 :
149+ close_brace_index = i
150+ break
151+ i += 1
152+
153+ if close_brace_index is None :
154+ return text , None , False
155+
156+ body = text [open_brace_index + 1 : close_brace_index ]
157+
158+ end = close_brace_index + 1
159+ if end < len (text ) and text [end ] == ";" :
160+ end += 1
161+
162+ return text [:declaration_start ] + text [end :], body , True
163+
164+
165+ def _remove_generated_export_blocks (text : str ) -> tuple [str , list [str ], str | None , bool ]:
166+ generated_block_re = re .compile (
167+ r"\n?(?:(?:// Export translator compatibility exports for adapter\n)+"
168+ r"export\s+const\s+exports\s*=\s*\{([\s\S]*?)\};\n*)?"
169+ r"// Export translator functions as ES module bindings for adapter\n"
170+ r"export\s*\{([^}]*)\};\s*\Z" ,
118171 re .MULTILINE ,
119172 )
120- text_without_generated = generated_metadata_block_re .sub ("" , text )
173+ m = generated_block_re .search (text )
174+ if m :
175+ return text [:m .start ()].rstrip (), _parse_generated_export_specs (m .group (2 )), m .group (1 ), True
121176
122- # Backward compatibility: remove old generated function-only trailing block
123- generated_block_re = re .compile (
124- r"\n?// Export translator functions as ES module bindings for adapter\n"
125- r"export\s*\{[^}]*\};\s*\Z" ,
177+ compatibility_only_re = re .compile (
178+ r"\n?(?:// Export translator compatibility exports for adapter\n)+"
179+ r"export\s+const\s+exports\s*=\s*\{([\s\S]*?)\};\s*\Z" ,
126180 re .MULTILINE ,
127181 )
128- new_text = generated_block_re .sub ("" , text_without_generated ).rstrip ()
182+ m = compatibility_only_re .search (text )
183+ if m :
184+ return text [:m .start ()].rstrip (), [], m .group (1 ), True
129185
130- candidate = new_text + export_snippet
131- if candidate == text or candidate + "\n " == text :
132- return text , False
186+ return text , [], None , False
187+
188+
189+ def append_exports (text : str ) -> tuple [str , bool ]:
190+ original_text = text
191+ text , old_generated_specs , old_generated_exports_body , removed_generated_blocks = _remove_generated_export_blocks (text )
192+ text , exports_body , removed_exports_object = _extract_and_remove_exports_object (text )
193+
194+ present = [fn for fn in TRANSLATOR_EXPORT_CANDIDATES if _is_function_defined (text , fn )]
195+ specs : list [str ] = []
196+ seen_specs : set [str ] = set ()
197+
198+ for fn in present :
199+ if fn not in seen_specs :
200+ seen_specs .add (fn )
201+ specs .append (fn )
202+
203+ compatibility_exports_body = exports_body or old_generated_exports_body
204+ if compatibility_exports_body is None and old_generated_specs :
205+ extra_specs = [spec for spec in old_generated_specs if spec not in seen_specs ]
206+ compatibility_exports_body = _build_exports_body_from_specs (extra_specs )
207+
208+ snippets = []
209+ if compatibility_exports_body and compatibility_exports_body .strip ():
210+ snippets .append (
211+ "\n // Export translator compatibility exports for adapter\n "
212+ + "export const exports = {"
213+ + compatibility_exports_body
214+ + "};\n "
215+ )
216+
217+ if specs :
218+ export_line = f"export {{ { ', ' .join (specs )} }};"
219+ snippets .append (
220+ "\n // Export translator functions as ES module bindings for adapter\n "
221+ + export_line
222+ + "\n "
223+ )
224+
225+ if not snippets :
226+ return text , text != original_text
227+
228+ candidate = text .rstrip () + "" .join (snippets )
229+ if candidate == original_text or candidate + "\n " == original_text :
230+ return original_text , False
133231
134232 return candidate , True
135233
0 commit comments