"""Fetch a ChatGPT/Claude *share link* and parse its embedded conversation JSON.
A pasted share URL should "just work": we GET the public share page, dig the conversation
out of the JSON the page ships to the browser (no headless browser, no API key), and map it
to the same `ParsedExport` the paste path returns (source="paste" → single-conversation flow).
Where the JSON lives (web-verified shapes; both have several historical variants we tolerate):
- ChatGPT: `` (Next.js) — or newer Remix/react-router
builds embedding `window.__remixContext = {…}` / `__reactRouterContext`. We grep all of them
for the message mapping (or `linear_conversation`) and reuse the parser's ChatGPT mapping
walk where possible.
- Claude: a JSON blob carrying `chat_messages: [{sender, text|content}]`, embedded in a
`
_NEXT_DATA_RE = re.compile(
r'', re.S | re.I)
# window.__remixContext = {…}; / __reactRouterContext = {…};
_REMIX_RE = re.compile(
r'(?:window\.)?(?:__remixContext|__reactRouterContext)\s*=\s*(\{.*?\})\s*;', re.S)
# generic fallback: any
_ANY_SCRIPT_RE = re.compile(r'', re.S | re.I)
def _json_blobs(html: str):
"""Yield candidate parsed-JSON objects embedded in the page, best matches first."""
seen = []
for rx in (_NEXT_DATA_RE, _REMIX_RE):
for m in rx.finditer(html):
obj = _try_json(m.group(1))
if obj is not None:
seen.append(obj)
# Last resort: scan every