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.
- givememd-0.1.0/.gitignore +16 -0
- givememd-0.1.0/LICENSE +21 -0
- givememd-0.1.0/PKG-INFO +181 -0
- givememd-0.1.0/README.md +154 -0
- givememd-0.1.0/givememd/__init__.py +39 -0
- givememd-0.1.0/givememd/__main__.py +3 -0
- givememd-0.1.0/givememd/cli.py +23 -0
- givememd-0.1.0/givememd/components.py +189 -0
- givememd-0.1.0/givememd/compose.py +337 -0
- givememd-0.1.0/givememd/datatables.py +272 -0
- givememd-0.1.0/givememd/decide.py +144 -0
- givememd-0.1.0/givememd/extract.py +474 -0
- givememd-0.1.0/givememd/fetch.py +256 -0
- givememd-0.1.0/givememd/jev.py +146 -0
- givememd-0.1.0/givememd/mcp_server.py +30 -0
- givememd-0.1.0/givememd/server.py +67 -0
- givememd-0.1.0/pyproject.toml +52 -0
- givememd-0.1.0/tests/fixtures/block_aws_waf.html +3 -0
- givememd-0.1.0/tests/fixtures/block_datadome.html +3 -0
- givememd-0.1.0/tests/fixtures/block_network_security.html +4 -0
- givememd-0.1.0/tests/fixtures/block_shop_interstitial.html +3 -0
- givememd-0.1.0/tests/fixtures/ok_docs.html +29 -0
- givememd-0.1.0/tests/fixtures/ok_listings.html +5 -0
- givememd-0.1.0/tests/fixtures/ok_tiny_static.html +3 -0
- givememd-0.1.0/tests/test_compose.py +112 -0
- givememd-0.1.0/tests/test_jev.py +77 -0
- givememd-0.1.0/tests/test_live.py +79 -0
- givememd-0.1.0/tests/test_unit.py +399 -0
|
@@ -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.
|
givememd-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
givememd-0.1.0/README.md
ADDED
|
@@ -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,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()
|