4040 "try_examples_outer_iframe" , # SciPy interactive-examples sandbox iframe
4141 "sidemenu" , # lxml.de project nav inside div.document
4242 "banner" , # lxml.de donation banner ("Like the tool? Help making it better!")
43- "sr-only" , # Tailwind/Bootstrap screen-reader-only content (e.g., modelcontextprotocol.io's "Documentation Index" llms.txt callout)
43+ "sr-only" ,
4444)
4545_CRUFT_IDS = (
4646 "indices-and-tables" , # Sphinx auto-generated bottom-of-index "Index/ModIndex/Search" stub
@@ -176,25 +176,6 @@ def extract_main_content(self, soup):
176176 return None
177177
178178
179- # -----------------------------------------------------------------------------
180- # Mintlify .md companion → HTML rendering
181- #
182- # Mintlify ships a `.md` companion for every doc page (linked from the site's
183- # llms.txt index). The .md is the canonical source — it has every language
184- # tab AND every OS variant, the rendered HTML hides Windows/etc. behind
185- # JavaScript-only Radix UI tabs that curl_cffi can't trigger. So
186- # MintlifyScraper fetches the .md and converts it to HTML.
187- #
188- # python-markdown can't parse Mintlify's MDX as-is — Tab/CodeGroup wrappers
189- # and fence-info-strings like ```bash macOS/Linux theme={null} need
190- # preprocessing before standard markdown rendering kicks in.
191- # -----------------------------------------------------------------------------
192-
193- # Maps each Mintlify MDX component name to how we transform it before
194- # python-markdown sees it.
195- # UNWRAP: drop the tags, keep dedented interior
196- # HEADING: replace with "## {title}" if title attribute present, else unwrap
197- # QUOTE: wrap interior lines as "> ..." blockquote
198179_MINTLIFY_MDX_COMPONENTS = {
199180 "Tabs" : "UNWRAP" ,
200181 "Tab" : "HEADING" ,
@@ -243,8 +224,6 @@ def repl(m):
243224 inner = textwrap .dedent (m .group (2 )).strip ("\n " )
244225 tm = _MINTLIFY_TITLE_ATTR_RE .search (attrs )
245226 if tm :
246- # Escape '#' so python-markdown doesn't read it as the ATX
247- # closing-marker syntax (e.g. '## C#' -> <h2>C</h2>).
248227 title = tm .group (1 ).replace ("#" , r"\#" )
249228 return f"\n \n ## { title } \n \n { inner } \n \n "
250229 return f"\n \n { inner } \n \n "
@@ -268,9 +247,6 @@ def repl(m):
268247
269248
270249def _mintlify_normalize_fences (md_text ):
271- """Strip Mintlify-specific fence-info-string metadata so python-markdown
272- can recognize the fences. Preserve any descriptive label (filename, OS
273- name) as a bolded line above the fence."""
274250 out = []
275251 for line in md_text .split ("\n " ):
276252 m = _MINTLIFY_FENCE_OPEN_RE .match (line )
@@ -287,7 +263,6 @@ def _mintlify_normalize_fences(md_text):
287263
288264
289265def render_mintlify_markdown (md_text ):
290- """Convert a Mintlify .md (markdown + MDX) to HTML."""
291266 md_text = _MINTLIFY_DOC_INDEX_RE .sub ("" , md_text , count = 1 )
292267 for name , action in _MINTLIFY_MDX_COMPONENTS .items ():
293268 if action == "UNWRAP" :
@@ -305,21 +280,6 @@ def render_mintlify_markdown(md_text):
305280
306281
307282class MintlifyScraper (BaseScraper ):
308- """For Mintlify-rendered docs (e.g., modelcontextprotocol.io).
309-
310- Mintlify uses Radix UI tabs to switch between language and OS variants —
311- only the active tab is server-rendered, the rest are JS-hydrated. So
312- fetching the rendered HTML loses every Windows-side code variant and any
313- inactive language tab. Fortunately Mintlify also publishes a `.md`
314- companion for every page (canonical source, linked from llms.txt),
315- which contains all variants verbatim. We fetch the .md instead and
316- render it to HTML on the fly.
317-
318- BFS discovery is bootstrapped from /llms.txt (which enumerates every
319- page) because the rendered .md has only sparse inline cross-references
320- — the site nav we'd normally crawl isn't part of the markdown.
321- """
322-
323283 async def collect_seed_urls (self , session ):
324284 parsed_seed = urlparse (self .url )
325285 seed_prefix = parsed_seed .path .rstrip ("/" )
@@ -351,17 +311,13 @@ def fetch_url_for(self, url):
351311 return u + ".md"
352312
353313 def transform_response (self , text , url ):
354- # Heuristic: HTML responses start with <!doctype or <html. Anything
355- # else we assume is the .md companion.
356314 head = text .lstrip ()[:200 ].lower ()
357315 if head .startswith ("<!doctype" ) or head .startswith ("<html" ):
358316 return text
359317 rendered = render_mintlify_markdown (text )
360318 return f"<html><body>{ rendered } </body></html>"
361319
362320 def extract_main_content (self , soup ):
363- # transform_response already returned a clean HTML doc; the body IS
364- # the article content.
365321 return soup .body if soup .body else soup
366322
367323
@@ -539,9 +495,6 @@ async def process_batch(batch_urls, session):
539495 return out
540496
541497 async with AsyncSession (impersonate = "chrome" ) as session :
542- # Optional bootstrap: scrapers that have an authoritative URL
543- # index (e.g. Mintlify's llms.txt) prepopulate the queue from it
544- # since BFS over the rendered markdown alone can miss most pages.
545498 if not self .resume and hasattr (self .scraper , "collect_seed_urls" ):
546499 try :
547500 extra = await self .scraper .collect_seed_urls (session )
@@ -619,9 +572,6 @@ async def fetch(
619572 if os .path .exists (filename ):
620573 return set ()
621574
622- # Optional: scraper can fetch a different URL (e.g. a .md companion)
623- # while the saved filename and BFS bookkeeping continue to track the
624- # original page URL.
625575 fetch_url = (
626576 self .scraper .fetch_url_for (url )
627577 if hasattr (self .scraper , "fetch_url_for" )
@@ -661,10 +611,6 @@ async def fetch(
661611 self ._429s_since_last_success = 0
662612
663613 content_type = response .headers .get ("content-type" , "" ).lower ()
664- # Skip the content-type filter when the scraper opts to
665- # transform the response — the .md companion is served as
666- # text/markdown or text/plain, which the transformer turns
667- # into proper HTML.
668614 if not has_response_transform and "text/html" not in content_type :
669615 self .stats ["scraped" ] = self .count_saved_files ()
670616 self .status_updated .emit (self .name , str (self .stats ["scraped" ]))
0 commit comments