Skip to content

feat(formats): add plain text and Markdown input - #21

Merged
chaliy merged 9 commits into
mainfrom
claude/project-thread-3z7tjs
Oct 27, 2026
Merged

chaliy merged 9 commits into
mainfrom
claude/project-thread-3z7tjs

Conversation

@chaliy

@chaliy chaliy commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

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

What changed

extract_document/parse_document now accept plain text and Markdown. That covers the CLI, extract_bytes, extract_url, Python, Node and WASM. This is the first of the roadmap's "cheap formats" PRs.

  • Plain text (text/plain, .txt, .text):
    • Always on, with no new dependencies.
    • Blank lines separate paragraphs. Single newlines become hard breaks.
    • The text goes through the IR as Node::Text, so the renderer's escaping applies to it like any other format. A <script> line in a .txt comes out escaped.
  • Markdown (text/markdown, text/x-markdown, .md/.markdown/.mdown/.mkd/.mkdn):
    • Behind a new markdown feature. It is part of all_formats, so the CLI, Python and Node get it, and it is enabled in the WASM package too.
    • Re-parsed, never passed through: pulldown-cmark parses it into IR v2 and the renderer writes it back out.
    • Supported syntax: GFM tables (with alignment), strikethrough, task lists, footnotes and definition lists. Soft breaks reflow into spaces.
    • YAML front matter is not rendered. Its title: becomes title; without one, the first # heading is used.
    • HTML blocks go through the HTML parser, or are dropped without the html feature. Inline HTML tags are dropped.
  • Limits: both formats use the call's Limits budget. Each line or event checks timeout, start tags count against max_nodes, and nesting is capped at max_nesting_depth. HTML blocks share the same budget.
  • Shared decoding:
    • UTF-8 with the BOM stripped, and UTF-16 LE/BE when there is a BOM. Anything else is decoded lossily with an invalid_utf8 warning.
    • CRLF/CR become LF. C0 controls other than tab and newline, and DEL, are dropped.
  • Detection: text is never sniffed from bytes, so unlabelled input stays UnsupportedFormat.
  • Format::PlainText now serializes as "text", the same as as_str().
  • Transforms run on text and Markdown documents as they do for other formats.

Why

Roadmap initiative "cheap formats". Plain text was listed as planned but returned UnsupportedFormat. Normalizing Markdown through the IR gives it the same security guarantees as every other format.

Before / After

Before:

$ markkit extract --file notes.txt
Error: Unsupported format: text

After:

$ printf 'line one\nline two\n' > notes.txt && markkit extract --file notes.txt
line one  
line two
$ printf 'Title\n=====\n\n* a\n\n<script>x()</script>\n' > doc.md && markkit extract --file doc.md
# Title

- a

Tests:

  • tests/text_integration.rs has 18 tests. 12 of them are security tests for the new threat T7 and invariant S18: control characters, invalid UTF-8/UTF-16, raw HTML, escaped plain-text markup, 100k-deep nesting, timeout/max_nodes, size limits and random bytes.
  • There are also unit tests in text/markdown (including the depth limit and max_nodes), a CLI test and a WASM node test.
  • All of these pass: the workspace with default features and with all features, the docx,spreadsheet,markdown no-HTTP build, clippy and check-okf.

Risk

Checklist

  • Tests added or updated
  • Backward compatibility considered (new feature flag, CHANGELOG Added entry, knowledge T7/S18)

🤖 Generated with Claude Code

Plain text (always on) goes through the IR: paragraphs on blank lines,
hard breaks on single newlines. Markdown (new `markdown` feature, in
all_formats) is re-parsed with pulldown-cmark into the IR and rendered
back, so raw HTML never passes through. Both decode UTF-8/UTF-16 BOMs,
drop control characters and warn on lossy decoding. Neither is sniffed
from bytes. Threat model T7, invariant S13.
@chaliy chaliy self-assigned this Oct 26, 2026
…-3z7tjs

# Conflicts:
#	CHANGELOG.md
#	README.md
#	crates/markkit/Cargo.toml
#	crates/markkit/src/extract.rs
#	crates/markkit/src/lib.rs
#	knowledge/log.md
#	knowledge/security/threat-model.md
…-3z7tjs

# Conflicts:
#	.github/workflows/ci.yml
#	crates/markkit/Cargo.toml
#	crates/markkit/src/extract.rs
#	crates/markkit/src/lib.rs
#	knowledge/log.md
#	knowledge/security/threat-model.md
…-3z7tjs

# Conflicts:
#	knowledge/log.md
#	knowledge/security/threat-model.md
@chaliy
chaliy merged commit 2e60aee into main Oct 27, 2026
13 checks passed
chaliy added a commit that referenced this pull request Oct 27, 2026
chaliy added a commit that referenced this pull request Oct 27, 2026
<!-- claude-projects-attribution: {"github_login":"chaliy"} -->
_Requested by **Михайло** · [project
thread](https://claude.ai/code/project/chan_01MUtksmx61ypcr6wBFEWsbM?thread=cmsg_01MUtksmx61ypcr6wBFEWsbMJ85i1gQtgReoJ4AEgQoFq7)_

## What changed
`extract_document` and `parse_document` now accept Jupyter notebooks
(`.ipynb`, `application/x-ipynb+json`, nbformat 4). This applies to the
CLI, Python, Node and WASM alike. It is the third "cheap formats" PR,
after #21 and #44.

- **Always on**, with no new dependencies (uses serde_json). Notebooks
are never sniffed from bytes.
- **Markdown cells** go through the Markdown parser, so raw HTML is
never copied. Without the `markdown` feature they become plain-text
paragraphs.
- **Code cells** become fenced blocks tagged with the kernel language
(sanitized). **Raw cells** become untagged blocks.
- **Outputs:**
  - `stream` and `text/plain` become ```` ```text ```` blocks.
  - `text/markdown` goes through the Markdown path.
- Errors render as `ename: evalue` plus the traceback, with ANSI escapes
stripped.
  - Images, HTML and widget outputs are skipped.
- **Title:** `metadata.title`, otherwise the first `# ` heading.
- **Limits:** each cell checks the call's `timeout` and counts against
`max_nodes`. Markdown cells share the same budget.
- **New API:** `Format::Notebook` (serialized as `"ipynb"`, the same as
`as_str()`), `Error::NotebookExtract`, `stats.cells`.

### Also fixes a limits bug from #44
Reported by the IR thread. With no format feature enabled
(`--no-default-features`), `extract_document` rendered without bounds.
Always-on text, CSV and notebook input therefore ignored
`max_output_size` and the final deadline check, and
`csv_integration::security_timeout_applies` failed.

The fix:
- Bounded rendering is now unconditional.
- Decoding is followed by a deadline check.
- CI now runs `cargo test -p markkit --no-default-features`.

## Why
This is the roadmap initiative "cheap formats". Notebooks are a common
input for agents, and their outputs often hold the result a reader
needs.

## Before / After
Before:
```
$ markkit extract --file analysis.ipynb
Error: Unsupported format
$ cargo test -p markkit --no-default-features --test csv_integration
test security_timeout_applies ... FAILED
```
After:
````
$ markkit extract --file analysis.ipynb
# Analysis

```python
print(1 + 1)
```

```text
2
```
$ cargo test -p markkit --no-default-features
(all pass)
````

Tests:
- `tests/notebook_integration.rs` has 11 tests: mapping, ANSI stripping,
metadata, detection, and security cases.
- The security cases cover malformed or 1000-deep JSON, wrong value
types, raw HTML, control characters, and the timeout and `max_nodes`
limits.
- There are unit tests and a WASM node test.
- Clippy and `markkit` tests pass in each of these feature sets:
`--no-default-features`, `markdown`, `docx,spreadsheet,markdown`,
default, and all features.
- `check-okf` is clean.

## Risk
- Low. The notebook support only adds behavior, and rich outputs are
dropped by design.
- The limits fix only affects builds without any format feature, which
previously ignored those limits.

## Checklist
- [x] Tests added or updated
- [x] Backward compatibility considered (CHANGELOG Added entry,
knowledge T7/S18 extended, log entry for the limits fix)

---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
@chaliy chaliy mentioned this pull request Oct 27, 2026
2 tasks done
chaliy added a commit that referenced this pull request Oct 27, 2026
## What changed
New `email` feature (in `all_formats`, so CLI and Python/Node bindings
get it) converts `.eml` / `message/rfc822` files to Markdown:
- Subject becomes the `#` heading and the title, followed by From / To /
Cc / Date lines.
- The plain-text body is preferred. When a message has only an HTML
body, it goes through the HTML parser with the same stripping and
hidden-content rules as web pages.
- Attachments and nested messages are listed by name, type and size,
never parsed.
- New `Format::Eml` (`"eml"`), `Error::EmailExtract`,
`ExtractionStats::attachments`.

The parser shares the call's `Limits` budget (nodes, timeout, output
cap). Header values are folded, so an encoded newline cannot inject a
line.

## Why
This is part of the cheap-formats roadmap initiative (text/Markdown #21,
CSV/TSV #44 and ipynb #50 are already merged). Email is a common input
for agents.

## Before / After
```
$ markkit extract --file a.eml
# before
Error: Unsupported format: unknown
# after
# Hi

**From:** Ada \<ada@example.com>  
**To:** rene@example.com

Hello \<b>there\</b>
```
I ran tests locally under all features, the default build,
`--no-default-features`, `docx,spreadsheet,markdown,email` and `email`
alone. Clippy passes with `-D warnings` on each, and the OKF check
passes.

## Risk
- Low: the feature is additive and opt-in; the default build is
unchanged.
- It depends on mail-parser 0.11 (`default-features = false`). 1.0 is a
fresh rewrite, so it is not adopted yet.

## Checklist
- [x] Tests added or updated: `email_integration` covers multipart,
HTML-only, nested and detection, plus security tests for garbage input,
header injection, deep multipart, attachment floods, input size and
`security_limits_apply` (timeout, max_nodes, output cap)
- [x] Backward compatibility considered: the change is additive. Threat
T7 and invariant S18 now cover email. Knowledge, CHANGELOG and README
are updated.

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

---
_Generated by [Claude
Code](https://claude.ai/code/session_011vT4a1LnUCTWbKHD7BAsFM)_
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