Skip to content

feat(html): Readability-style main-content detection - #38

Merged
chaliy merged 14 commits into
mainfrom
claude/project-thread-n410ea
Oct 27, 2026
Merged

chaliy merged 14 commits into
mainfrom
claude/project-thread-n410ea

Conversation

@chaliy

@chaliy chaliy commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Requested by Михайло · project thread

What changed

  • New opt-in MarkkitBuilder::main_content(true) / CLI --main-content: keeps only the article body of a web page and drops top bars, banners, sidebars, comments, share bars and related-link blocks.
  • Third strategy next to include selectors (which still win) and the full body. When nothing article-like scores, it falls back to the full body, so it never empties a page that has content.
  • Readability-style scorer (html/readability.rs): paragraph text length and commas credited up three ancestor levels, tag and class/id weights, link-density scaling, related siblings kept. Algorithm documented in knowledge/formats/html-extraction.md.
  • Works with provenance (track_sources) and with the base URL / metadata from feat(html): extract page metadata from meta, Open Graph and JSON-LD #20 and feat(html): resolve relative URLs against a base URL #31.

Last of three PRs for the roadmap's "Readability and metadata" item.

Why

Web pages leaked nav, ads and comments into the Markdown unless the caller wrote CSS selectors per site.

Before / After

tests/fixtures/html/news_page.html (a news page with a top bar, banner, sidebar, share bar, comments and related links):

Before (markkit extract --file news_page.html):

[Home](/) [Tech](/tech) [Science](/science) [Log in](/login)  Subscribe today and save 50% on your first year of unlimited access!

## What changed
...
[Share on X](https://x.example/share) [Share on Facebook](https://fb.example/share)
...
### Most read
...
First comment, this is great news, finally, I have waited for generators for years.

After (--main-content): only the article, exactly tests/fixtures/html/news_page.main.md:

## What changed

The Rust project shipped its 2026 edition on Thursday, ...

```toml
edition = "2026"

The full release notes, including a migration guide, are available on the project blog.


## Risk
- Low. Off by default; default output unchanged.
- Heuristic: an article whose container is classed like boilerplate (e.g. only `class="sidebar"`) can be dropped; the fallback and the `content`/`article`/`main`/`post` override limit this. The eval harness will measure it on real pages.
- DoS (threat model T2g): one iterative traversal, no recursion; scored paragraphs are leaf blocks so text measurement is linear; link density only for the top 10 candidates and the winner's siblings. Tests: 5,000-deep nesting, 20,000 paragraphs, 5,000 sibling containers, scripts still stripped.

## Checklist
- [x] Tests added or updated (`tests/html_main_content.rs` incl. security/perf cases and provenance, golden fixture `news_page.main.md`, unit tests)
- [x] Backward compatibility considered (opt-in; no API removed)

---
_Generated by [Claude Code](https://claude.ai/code/session_01CEs5BsitrunoTsYY5SYtFy)_

chaliy and others added 14 commits October 26, 2026 23:34
extract_document on HTML fills Extraction::title and new
Extraction::metadata (description, author, published, language,
canonical URL, site name, image). Values are cleaned and capped, URL
fields drop non-http(s) schemes, JSON-LD reading is bounded. CLI --json
includes format, title and metadata for files and stdin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEs5BsitrunoTsYY5SYtFy
Links, images and metadata URLs resolve against MarkkitBuilder::base_url
(CLI --base-url) with the page <base href> on top; extract_url uses the
final response URL. Only scheme-less references change; page bases are
limited to http(s). Adds Markkit::extract_url_document.
@chaliy chaliy self-assigned this Oct 27, 2026
@chaliy
chaliy merged commit f14bd37 into main Oct 27, 2026
13 checks passed
chaliy added a commit that referenced this pull request Oct 27, 2026
…WASM (#40)

<!-- claude-projects-attribution: {"github_login":"chaliy"} -->
_Requested by **Михайло** · [project
thread](https://claude.ai/code/project/chan_01MUtksmx61ypcr6wBFEWsbM?thread=cmsg_01MUtksmx61ypcr6wBFEWsbMGqDAMfmyQqFCQtq87yxmfS)_

## What changed
This is the follow-up to #27. Every `markkit::Limits` field can now be
set outside Rust:
- **CLI**: `markkit extract` gets a "Limits" help section with one flag
per limit: `--max-input-size` (`--max-size` still works as an alias),
`--max-output-size`, `--timeout` (seconds, fractions allowed),
`--max-nesting-depth`, `--max-nodes`, the archive and sheet flags, and
`--max-redirects`.
- **Python**: new `markkit.Limits(...)` class, a frozen keyword-only
constructor with readable attributes. Bad values raise
`ValueError`/`OverflowError`. Every function accepts `limits=`.
- **Node.js**: `MarkkitOptions` gets flat keys `timeout` (seconds,
float, and it wins over `maxProcessingSecs`), `maxOutputSize`,
`maxNodes` and `maxNestingDepth`. A negative or NaN `timeout` is
rejected.
- **WASM**: `Options` gets `maxOutputSize`, `maxNodes` and
`maxNestingDepth`. There is no `timeout`, because wasm32 has no clock.
Unknown keys throw, so passing `timeout` fails loudly instead of being
ignored.
- **Parity**: Node and WASM options also get `baseUrl` and `mainContent`
(#31, #38), and Python `extract_html_sync_with_options` gets `base_url=`
and `main_content=`, so the bindings match the core builder.

The binding READMEs, the knowledge runtime docs and parity table,
CHANGELOG and log are updated.

## Why
The user asked for limits on every aspect. #27 added them to the core,
and this PR makes them reachable from every runtime.

## Before / After
Before: the CLI had only `--max-size`. Python had no limit arguments,
and Node and WASM had no output, node or depth caps.
After (CLI, table-amplification input that renders 12 MB of Markdown):
```
$ markkit extract --file amp.html --max-output-size 1000000
Error: Resource limit exceeded: output exceeds max_output_size 1000000
$ markkit extract --file amp.html --timeout 0
Error: Resource limit exceeded: processing exceeded timeout of 0ns
```
Python smoke run:
```
>>> markkit.extract_html_sync('<p>x</p>'*100, limits=markkit.Limits(max_output_size=10))
Resource limit exceeded: output exceeds max_output_size 10   # raised
>>> markkit.Limits(timeout=2.5)
Limits(max_input_size=52428800, max_output_size=134217728, timeout=2.5, max_nesting_depth=256, ...)
```
Test results: Node `options.test.cjs` passes 9 of 9, WASM tests pass 19
of 19, and CLI unit and integration tests pass.

## Risk
- Low. All additions are optional, and defaults are unchanged.
`--max-size` is kept.

## Checklist
- [x] Tests added or updated
- [x] Backward compatibility considered

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant