givememd 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .venv/
6
+ venv/
7
+ dist/
8
+ build/
9
+ # benchmark data and outputs: other sites' content, never committed
10
+ bench/out_*/
11
+ eval/results/
12
+ *.html.gz
13
+ # node
14
+ node_modules/
15
+ js/dist/
16
+ js/*.tgz
givememd-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ishanray
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.5
2
+ Name: givememd
3
+ Version: 0.1.0
4
+ Summary: Turn any web page into clean, token-lean markdown for LLMs. Fetches from your own machine.
5
+ Project-URL: Homepage, https://github.com/ishanray/givememd
6
+ Project-URL: Repository, https://github.com/ishanray/givememd
7
+ Project-URL: Issues, https://github.com/ishanray/givememd/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,html-to-markdown,llm,markdown,mcp,scraping,web
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
15
+ Classifier: Topic :: Text Processing :: Markup :: HTML
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: curl-cffi>=0.7
18
+ Requires-Dist: playwright>=1.40
19
+ Requires-Dist: selectolax>=0.3.27
20
+ Requires-Dist: tiktoken>=0.7
21
+ Provides-Extra: browser
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Provides-Extra: mcp
25
+ Requires-Dist: mcp>=2.0; extra == 'mcp'
26
+ Description-Content-Type: text/markdown
27
+
28
+ <p align="center"><img src="https://raw.githubusercontent.com/ishanray/givememd/main/docs/banner.png" alt="givememd: a page full of scripts, ads, popups and cookie banners goes in; only the content comes out as markdown" width="100%"></p>
29
+
30
+ # givememd
31
+
32
+ Turn any web page into clean, token-lean markdown for LLMs.
33
+
34
+ ```sh
35
+ givememd https://www.zillow.com/homes/Austin,-TX_rb/
36
+ ```
37
+
38
+ givememd fetches the page (escalating from a plain request to a headless browser only when needed), works out
39
+ what is content and what is chrome, and returns markdown that keeps the facts and drops the noise: listings hidden in
40
+ a page's app state become tables, menus and cookie banners disappear, and walled pages are reported as walled instead of
41
+ being passed off as content.
42
+
43
+ ## Requirements
44
+
45
+ givememd ships for both **Python 3.10+** and **Node 20+**; pick one. Both run on macOS (Intel and Apple silicon),
46
+ Linux (x86-64 and ARM, glibc and musl) and Windows (x64 and ARM). No compiler, API key or account is needed.
47
+
48
+ ```sh
49
+ pip install givememd # or: npm i givememd (or no install: npx givememd <url>)
50
+ ```
51
+ Or straight from a clone of this repository, with the normal install for either language:
52
+ ```sh
53
+ git clone https://github.com/ishanray/givememd && cd givememd
54
+ pip install . # Python, then: givememd <url>
55
+ npm install # Node (installs and builds js/), then: npx givememd <url>
56
+ ```
57
+
58
+ **JavaScript-heavy pages work out of the box.** Most pages are read with one plain request: 86% of the top sites we
59
+ could read, and 92% of smaller sites. Apps like Reddit, Amazon's homepage or character.ai need a real browser, and
60
+ givememd uses one automatically:
61
+ 1. **An installed browser:** Chrome, Edge, Brave or Chromium, driven headlessly. Nothing to download.
62
+ 2. **Otherwise, a one-time download:** on the first page that needs JavaScript, givememd fetches Playwright's
63
+ headless Chromium (about 100 MB download, 200 MB on disk) and reuses it afterwards.
64
+ `GIVEMEMD_CHROME=/path/to/chrome` picks a browser; `GIVEMEMD_NO_BROWSER_DOWNLOAD=1` disables the download.
65
+ 3. **Optional, faster:** [Lightpanda](https://github.com/lightpanda-io/browser) (AGPL, not bundled), set with
66
+ `GIVEMEMD_LIGHTPANDA=/path/to/lightpanda`.
67
+
68
+ Using it from Claude or other agents (MCP) needs `pip install "givememd[mcp]"` in Python; it's included in Node.
69
+
70
+ **Your connection is the fetcher.** Run locally (CLI, library or MCP server), every request comes from your machine and
71
+ your IP, which reads more of the web than a datacenter does. Nothing needs configuring. The optional settings are
72
+ `GIVEMEMD_ALLOWED_PORTS` (default `80,443`), `GIVEMEMD_MAX_BYTES` (default 15 MB) and, in Node,
73
+ `GIVEMEMD_STRICT_SSRF=1`. A public server needs more; see [SECURITY.md](https://github.com/ishanray/givememd/blob/main/SECURITY.md).
74
+
75
+ ## Use
76
+
77
+ **CLI**
78
+ ```sh
79
+ givememd https://react.dev/reference/react/useState # markdown to stdout (Node: npx givememd ...)
80
+ givememd amazon.com/dp/B0D1XD1ZV3 --no-links --trace # trace shows which fetch tier was needed
81
+ ```
82
+
83
+ **MCP** (runs on your machine, so pages are fetched from your own connection)
84
+ ```sh
85
+ claude mcp add givememd -- npx -y givememd-mcp # Node
86
+ claude mcp add givememd -- uvx --from 'givememd[mcp]' givememd-mcp # Python
87
+ ```
88
+ It exposes one tool, `read_url(url, links)`.
89
+
90
+ **HTTP server** (the `givememd.com/<url>` shape, for local use)
91
+ ```sh
92
+ givememd-server --port 8787
93
+ curl http://127.0.0.1:8787/en.wikipedia.org/wiki/Markdown
94
+ curl "http://127.0.0.1:8787/https://react.dev/reference/react/useState?links=0"
95
+ ```
96
+ Responses carry `X-Givememd-Tier`, `X-Givememd-Source` and `X-Givememd-Partial`; the Python server also sends
97
+ `X-Givememd-Tokens`. See [SECURITY.md](https://github.com/ishanray/givememd/blob/main/SECURITY.md) before exposing anything publicly.
98
+
99
+ **Library**
100
+ ```python
101
+ from givememd import read
102
+ md, result = read("https://docs.python.org/3/library/pathlib.html")
103
+ print(result.tier, md[:500])
104
+ ```
105
+ ```js
106
+ import { read, convert } from 'givememd';
107
+ const { markdown, result } = await read('https://docs.python.org/3/library/pathlib.html');
108
+ const md = convert(html, 'https://example.com/page', { links: false }); // HTML you already have
109
+ ```
110
+
111
+ The Node package is a TypeScript port. CI requires byte-identical output to Python on shared fixtures, and on 300 real
112
+ top-site pages 97% of outputs are identical. It doesn't yet include the experimental component mode; see
113
+ [js/README.md](https://github.com/ishanray/givememd/blob/main/js/README.md).
114
+
115
+ ## How it works
116
+
117
+ 1. **Fetch ladder.** A plain HTTP request with a browser-like TLS fingerprint (asking for `text/markdown` first; sites
118
+ that serve their own markdown are passed through) → Lightpanda → headless Chromium. A tier is skipped unless the previous
119
+ one got a bot wall, an empty JavaScript shell, or a page whose content only appears after rendering.
120
+ 2. **Is this real content?** Every response is classified `ok` / `blocked` / `needs_js`: vendor challenge markers
121
+ (Cloudflare, DataDome, AWS WAF, PerimeterX…), login walls, block statuses, empty shells. When every tier hits a wall,
122
+ givememd returns only the page's own metadata, marked `partial:`, rather than a login form.
123
+ 3. **Extraction.**
124
+ - Metadata and JSON-LD go into a compact frontmatter.
125
+ - Record arrays embedded in the page (Next.js state, JSON-LD lists) become markdown tables. Variants (colorways, sizes)
126
+ collapse into one row per item, and opaque IDs are dropped.
127
+ - The DOM is pruned of chrome and hidden UI. Collapsed "see more" content, tab panels and React-streamed segments are kept.
128
+ - Link-heavy blocks keep their headlines as one compact line and lose their menu labels.
129
+ - The result is de-duplicated, interface text is removed, links are cleaned of tracking parameters, and listing cards
130
+ render as single lines.
131
+ 4. **Optional component mode** (`convert(..., mode='components-light')`, experimental). The page is split into small
132
+ sections, each matched to one of 121 catalog components (product header, spec table, recipe, FAQ, navigation…). Each
133
+ section is filled and rendered with a fixed layout, so similar pages read the same way. Labels come from rules, plus
134
+ optionally [TypeSafe](https://typesafe.ai)'s Jev decision model (`GIVEMEMD_JEV=1` and `TYPESAFE_API_KEY`).
135
+
136
+ ## How it compares
137
+
138
+ Measured on 2026-10-30 against Jina Reader (`r.jina.ai`, no key) and defuddle.md. Each tool was run on the same URLs:
139
+ a homepage plus one inner page per site, fetched from one datacenter IP. Details and caveats are in
140
+ [docs/BENCHMARK.md](https://github.com/ishanray/givememd/blob/main/docs/BENCHMARK.md).
141
+
142
+ | Set | URLs | givememd vs Jina (win / tie / loss) | givememd vs defuddle (win / tie / loss) | Median tokens: givememd vs Jina |
143
+ |---|---|---|---|---|
144
+ | Top sites (Tranco top 2,000 + AI-cited) | 3,584 | 66 / 15 / 19% | 74 / 20 / 6% | 1,034 vs 4,135 |
145
+ | Smaller sites (Tranco ranks 2k–1M) | 795 | 65 / 15 / 20% | 76 / 18 / 7% | 1,027 vs 4,525 |
146
+ | Indie blogs (Kagi Small Web) | 64–300 | 58 / 39 / 3% | 42 / 56 / 2% | 1,059 vs 1,365 |
147
+
148
+ These scores are automatic. A page counts as "usable" if it has real content rather than a wall or an error, judged by
149
+ the same check for every tool. Facts are recall of titles, headings, paragraph sentences and JSON-LD from the raw page,
150
+ plus sentences that at least two tools agree on. Most of our remaining losses are bot walls against a datacenter IP,
151
+ and portal pages where keeping every menu scores more facts.
152
+
153
+ ## Develop
154
+
155
+ ```sh
156
+ pip install -e ".[dev,mcp]"
157
+ pytest # offline unit tests
158
+ pytest -m live # end-to-end: CLI, HTTP server and MCP over stdio, against the real web
159
+ python bench/fetch_pages.py # build the local page cache (other sites' pages are never committed)
160
+ python bench/regress.py # answer-key recall and tokens on 25 pages
161
+ python eval/run.py -v # 19 hand-labeled pages: content lost, junk leaked, fields, component labels
162
+
163
+ npm ci # at the repo root; builds the Node package in js/
164
+ npm test # unit tests + byte-for-byte parity with the Python goldens
165
+ npm run test:live # end-to-end: CLI, HTTP server and MCP over stdio
166
+ ```
167
+ A change to extraction or detection must land in both `givememd/` and `js/src/`; `js/parity/` has the comparison tools.
168
+ The large benchmark (top, smaller and indie sites against competitors) lives in `bench/top/`; see
169
+ [docs/BENCHMARK.md](https://github.com/ishanray/givememd/blob/main/docs/BENCHMARK.md). Contributions are welcome; see [CONTRIBUTING.md](https://github.com/ishanray/givememd/blob/main/CONTRIBUTING.md).
170
+
171
+ ## Roadmap
172
+
173
+ - A browser extension and a relay that lets cloud agents fetch through their owner's browser. This is how bot-walled
174
+ and logged-in pages become readable without proxies.
175
+ - A hosted `givememd.com/<url>` with caching.
176
+ - Caching component labels per site layout, so model-assisted labeling costs almost nothing per page.
177
+
178
+ ## License
179
+
180
+ MIT. Lightpanda (AGPL) and Chromium are optional and installed separately; Jev is an optional third-party API.
181
+ Product names in the benchmark belong to their owners.
@@ -0,0 +1,154 @@
1
+ <p align="center"><img src="https://raw.githubusercontent.com/ishanray/givememd/main/docs/banner.png" alt="givememd: a page full of scripts, ads, popups and cookie banners goes in; only the content comes out as markdown" width="100%"></p>
2
+
3
+ # givememd
4
+
5
+ Turn any web page into clean, token-lean markdown for LLMs.
6
+
7
+ ```sh
8
+ givememd https://www.zillow.com/homes/Austin,-TX_rb/
9
+ ```
10
+
11
+ givememd fetches the page (escalating from a plain request to a headless browser only when needed), works out
12
+ what is content and what is chrome, and returns markdown that keeps the facts and drops the noise: listings hidden in
13
+ a page's app state become tables, menus and cookie banners disappear, and walled pages are reported as walled instead of
14
+ being passed off as content.
15
+
16
+ ## Requirements
17
+
18
+ givememd ships for both **Python 3.10+** and **Node 20+**; pick one. Both run on macOS (Intel and Apple silicon),
19
+ Linux (x86-64 and ARM, glibc and musl) and Windows (x64 and ARM). No compiler, API key or account is needed.
20
+
21
+ ```sh
22
+ pip install givememd # or: npm i givememd (or no install: npx givememd <url>)
23
+ ```
24
+ Or straight from a clone of this repository, with the normal install for either language:
25
+ ```sh
26
+ git clone https://github.com/ishanray/givememd && cd givememd
27
+ pip install . # Python, then: givememd <url>
28
+ npm install # Node (installs and builds js/), then: npx givememd <url>
29
+ ```
30
+
31
+ **JavaScript-heavy pages work out of the box.** Most pages are read with one plain request: 86% of the top sites we
32
+ could read, and 92% of smaller sites. Apps like Reddit, Amazon's homepage or character.ai need a real browser, and
33
+ givememd uses one automatically:
34
+ 1. **An installed browser:** Chrome, Edge, Brave or Chromium, driven headlessly. Nothing to download.
35
+ 2. **Otherwise, a one-time download:** on the first page that needs JavaScript, givememd fetches Playwright's
36
+ headless Chromium (about 100 MB download, 200 MB on disk) and reuses it afterwards.
37
+ `GIVEMEMD_CHROME=/path/to/chrome` picks a browser; `GIVEMEMD_NO_BROWSER_DOWNLOAD=1` disables the download.
38
+ 3. **Optional, faster:** [Lightpanda](https://github.com/lightpanda-io/browser) (AGPL, not bundled), set with
39
+ `GIVEMEMD_LIGHTPANDA=/path/to/lightpanda`.
40
+
41
+ Using it from Claude or other agents (MCP) needs `pip install "givememd[mcp]"` in Python; it's included in Node.
42
+
43
+ **Your connection is the fetcher.** Run locally (CLI, library or MCP server), every request comes from your machine and
44
+ your IP, which reads more of the web than a datacenter does. Nothing needs configuring. The optional settings are
45
+ `GIVEMEMD_ALLOWED_PORTS` (default `80,443`), `GIVEMEMD_MAX_BYTES` (default 15 MB) and, in Node,
46
+ `GIVEMEMD_STRICT_SSRF=1`. A public server needs more; see [SECURITY.md](https://github.com/ishanray/givememd/blob/main/SECURITY.md).
47
+
48
+ ## Use
49
+
50
+ **CLI**
51
+ ```sh
52
+ givememd https://react.dev/reference/react/useState # markdown to stdout (Node: npx givememd ...)
53
+ givememd amazon.com/dp/B0D1XD1ZV3 --no-links --trace # trace shows which fetch tier was needed
54
+ ```
55
+
56
+ **MCP** (runs on your machine, so pages are fetched from your own connection)
57
+ ```sh
58
+ claude mcp add givememd -- npx -y givememd-mcp # Node
59
+ claude mcp add givememd -- uvx --from 'givememd[mcp]' givememd-mcp # Python
60
+ ```
61
+ It exposes one tool, `read_url(url, links)`.
62
+
63
+ **HTTP server** (the `givememd.com/<url>` shape, for local use)
64
+ ```sh
65
+ givememd-server --port 8787
66
+ curl http://127.0.0.1:8787/en.wikipedia.org/wiki/Markdown
67
+ curl "http://127.0.0.1:8787/https://react.dev/reference/react/useState?links=0"
68
+ ```
69
+ Responses carry `X-Givememd-Tier`, `X-Givememd-Source` and `X-Givememd-Partial`; the Python server also sends
70
+ `X-Givememd-Tokens`. See [SECURITY.md](https://github.com/ishanray/givememd/blob/main/SECURITY.md) before exposing anything publicly.
71
+
72
+ **Library**
73
+ ```python
74
+ from givememd import read
75
+ md, result = read("https://docs.python.org/3/library/pathlib.html")
76
+ print(result.tier, md[:500])
77
+ ```
78
+ ```js
79
+ import { read, convert } from 'givememd';
80
+ const { markdown, result } = await read('https://docs.python.org/3/library/pathlib.html');
81
+ const md = convert(html, 'https://example.com/page', { links: false }); // HTML you already have
82
+ ```
83
+
84
+ The Node package is a TypeScript port. CI requires byte-identical output to Python on shared fixtures, and on 300 real
85
+ top-site pages 97% of outputs are identical. It doesn't yet include the experimental component mode; see
86
+ [js/README.md](https://github.com/ishanray/givememd/blob/main/js/README.md).
87
+
88
+ ## How it works
89
+
90
+ 1. **Fetch ladder.** A plain HTTP request with a browser-like TLS fingerprint (asking for `text/markdown` first; sites
91
+ that serve their own markdown are passed through) → Lightpanda → headless Chromium. A tier is skipped unless the previous
92
+ one got a bot wall, an empty JavaScript shell, or a page whose content only appears after rendering.
93
+ 2. **Is this real content?** Every response is classified `ok` / `blocked` / `needs_js`: vendor challenge markers
94
+ (Cloudflare, DataDome, AWS WAF, PerimeterX…), login walls, block statuses, empty shells. When every tier hits a wall,
95
+ givememd returns only the page's own metadata, marked `partial:`, rather than a login form.
96
+ 3. **Extraction.**
97
+ - Metadata and JSON-LD go into a compact frontmatter.
98
+ - Record arrays embedded in the page (Next.js state, JSON-LD lists) become markdown tables. Variants (colorways, sizes)
99
+ collapse into one row per item, and opaque IDs are dropped.
100
+ - The DOM is pruned of chrome and hidden UI. Collapsed "see more" content, tab panels and React-streamed segments are kept.
101
+ - Link-heavy blocks keep their headlines as one compact line and lose their menu labels.
102
+ - The result is de-duplicated, interface text is removed, links are cleaned of tracking parameters, and listing cards
103
+ render as single lines.
104
+ 4. **Optional component mode** (`convert(..., mode='components-light')`, experimental). The page is split into small
105
+ sections, each matched to one of 121 catalog components (product header, spec table, recipe, FAQ, navigation…). Each
106
+ section is filled and rendered with a fixed layout, so similar pages read the same way. Labels come from rules, plus
107
+ optionally [TypeSafe](https://typesafe.ai)'s Jev decision model (`GIVEMEMD_JEV=1` and `TYPESAFE_API_KEY`).
108
+
109
+ ## How it compares
110
+
111
+ Measured on 2026-10-30 against Jina Reader (`r.jina.ai`, no key) and defuddle.md. Each tool was run on the same URLs:
112
+ a homepage plus one inner page per site, fetched from one datacenter IP. Details and caveats are in
113
+ [docs/BENCHMARK.md](https://github.com/ishanray/givememd/blob/main/docs/BENCHMARK.md).
114
+
115
+ | Set | URLs | givememd vs Jina (win / tie / loss) | givememd vs defuddle (win / tie / loss) | Median tokens: givememd vs Jina |
116
+ |---|---|---|---|---|
117
+ | Top sites (Tranco top 2,000 + AI-cited) | 3,584 | 66 / 15 / 19% | 74 / 20 / 6% | 1,034 vs 4,135 |
118
+ | Smaller sites (Tranco ranks 2k–1M) | 795 | 65 / 15 / 20% | 76 / 18 / 7% | 1,027 vs 4,525 |
119
+ | Indie blogs (Kagi Small Web) | 64–300 | 58 / 39 / 3% | 42 / 56 / 2% | 1,059 vs 1,365 |
120
+
121
+ These scores are automatic. A page counts as "usable" if it has real content rather than a wall or an error, judged by
122
+ the same check for every tool. Facts are recall of titles, headings, paragraph sentences and JSON-LD from the raw page,
123
+ plus sentences that at least two tools agree on. Most of our remaining losses are bot walls against a datacenter IP,
124
+ and portal pages where keeping every menu scores more facts.
125
+
126
+ ## Develop
127
+
128
+ ```sh
129
+ pip install -e ".[dev,mcp]"
130
+ pytest # offline unit tests
131
+ pytest -m live # end-to-end: CLI, HTTP server and MCP over stdio, against the real web
132
+ python bench/fetch_pages.py # build the local page cache (other sites' pages are never committed)
133
+ python bench/regress.py # answer-key recall and tokens on 25 pages
134
+ python eval/run.py -v # 19 hand-labeled pages: content lost, junk leaked, fields, component labels
135
+
136
+ npm ci # at the repo root; builds the Node package in js/
137
+ npm test # unit tests + byte-for-byte parity with the Python goldens
138
+ npm run test:live # end-to-end: CLI, HTTP server and MCP over stdio
139
+ ```
140
+ A change to extraction or detection must land in both `givememd/` and `js/src/`; `js/parity/` has the comparison tools.
141
+ The large benchmark (top, smaller and indie sites against competitors) lives in `bench/top/`; see
142
+ [docs/BENCHMARK.md](https://github.com/ishanray/givememd/blob/main/docs/BENCHMARK.md). Contributions are welcome; see [CONTRIBUTING.md](https://github.com/ishanray/givememd/blob/main/CONTRIBUTING.md).
143
+
144
+ ## Roadmap
145
+
146
+ - A browser extension and a relay that lets cloud agents fetch through their owner's browser. This is how bot-walled
147
+ and logged-in pages become readable without proxies.
148
+ - A hosted `givememd.com/<url>` with caching.
149
+ - Caching component labels per site layout, so model-assisted labeling costs almost nothing per page.
150
+
151
+ ## License
152
+
153
+ MIT. Lightpanda (AGPL) and Chromium are optional and installed separately; Jev is an optional third-party API.
154
+ Product names in the benchmark belong to their owners.
@@ -0,0 +1,39 @@
1
+ import re
2
+
3
+ from .extract import convert
4
+ from .fetch import TIERS, UnsafeURL, fetch
5
+
6
+
7
+ def read(target, links=True, tiers=None):
8
+ """URL -> (markdown, Result). Raises UnsafeURL for private/invalid targets."""
9
+ r = fetch(target, tiers=tiers)
10
+ if r.partial:
11
+ return convert(r.body, r.url, links=False, meta_only=True), r
12
+ if not r.ok:
13
+ return None, r
14
+ if 'markdown' in r.content_type:
15
+ return clean_site_markdown(r.body), r
16
+ md = convert(r.body, r.url, links=links)
17
+ later = [n for n, _ in TIERS[[n for n, _ in TIERS].index(r.tier) + 1:]] if r.tier in [n for n, _ in TIERS] else []
18
+ if later and len(body_text(md)) < 200 and r.body.count('<script') >= 3:
19
+ # the page "loaded" but its content is rendered by JavaScript (streamed payloads, app shells): render it
20
+ r2 = fetch(target, tiers=later, use_cache=False)
21
+ if r2.ok and 'markdown' not in r2.content_type:
22
+ md2 = convert(r2.body, r2.url, links=links)
23
+ if len(body_text(md2)) > len(body_text(md)):
24
+ r2.trace = r.trace + ['extraction was empty; rendered'] + r2.trace
25
+ return md2, r2
26
+ return md, r
27
+
28
+
29
+ def clean_site_markdown(md):
30
+ """Site-served markdown is often MDX: drop heading anchors and bare component tags, keep their content."""
31
+ md = re.sub(r'\s*\{/\*.*?\*/\}', '', md)
32
+ md = re.sub(r'(?m)^[ \t]*</?[A-Z][\w.]*(\s[^>]*)?/?>[ \t]*\n', '', md)
33
+ return re.sub(r'\n{3,}', '\n\n', md)
34
+
35
+
36
+ def body_text(md):
37
+ """Text after the frontmatter, without markdown punctuation."""
38
+ parts = md.split('---', 2)
39
+ return re.sub(r'[#*_`>|\-\s]+', ' ', parts[2] if len(parts) == 3 else md).strip()
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,23 @@
1
+ """givememd <url> [--no-links] [--trace] -- print LLM-ready markdown for a web page (fetched from this machine)."""
2
+ import sys
3
+
4
+ from . import UnsafeURL, read
5
+
6
+
7
+ def main():
8
+ args = [a for a in sys.argv[1:] if not a.startswith('--')]
9
+ if not args or '--help' in sys.argv or '-h' in sys.argv:
10
+ print(__doc__.strip()); sys.exit(0 if args or '--help' in sys.argv or '-h' in sys.argv else 2)
11
+ try:
12
+ md, r = read(args[0], links='--no-links' not in sys.argv)
13
+ except UnsafeURL as e:
14
+ sys.exit(f'refused: {e}')
15
+ if '--trace' in sys.argv:
16
+ print('\n'.join(r.trace), file=sys.stderr)
17
+ if md is None:
18
+ sys.exit(f'could not read {r.url}')
19
+ print(md)
20
+
21
+
22
+ if __name__ == '__main__':
23
+ main()