Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1cc3236
feat(formats): add plain text and Markdown input
chaliy Oct 26, 2026
7e3a233
fix(text): use as_chunks for UTF-16 decoding (clippy)
chaliy Oct 26, 2026
ba30062
chore(knowledge): scope S13 to markup copying, note text escaping res…
chaliy Oct 26, 2026
510056e
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
3b0b504
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
3526d61
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
62afeb8
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
573003f
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
511f20a
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
0ee47be
feat(formats): add CSV and TSV input
chaliy Oct 26, 2026
83a058e
chore: restart branch after #21 squash-merge
chaliy Oct 27, 2026
1340655
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
911360a
feat(formats): add Jupyter notebook input
chaliy Oct 26, 2026
76ea39d
chore: restart branch after #44 squash-merge
chaliy Oct 27, 2026
68ab6e7
fix(limits): enforce output cap and timeout in every feature set
chaliy Oct 27, 2026
a84a1e3
Merge remote-tracking branch 'origin/main' into pr50
chaliy Oct 27, 2026
03b3e3f
Merge remote-tracking branch 'origin/main' into pr50
chaliy Oct 27, 2026
3942d5e
Merge remote-tracking branch 'origin/claude/project-thread-3z7tjs' in…
chaliy Oct 27, 2026
2af2a1d
feat(formats): add email (EML) input
chaliy Oct 26, 2026
3b15b45
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
05f1cfa
Merge remote-tracking branch 'origin/claude/project-thread-3z7tjs' in…
chaliy Oct 27, 2026
90cc15c
feat(formats): add ODT input
chaliy Oct 27, 2026
0ae4874
Merge remote-tracking branch 'origin/main' into claude/project-thread…
chaliy Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ jobs:
fi
# Always-on formats (text, CSV, notebooks) must keep every limit with no format feature.
cargo test -p markkit --no-default-features
cargo test -p markkit --no-default-features --features docx,spreadsheet,markdown,email
if cargo tree -p markkit --no-default-features --features docx,spreadsheet,markdown,email -e normal | grep -E 'reqwest|hyper'; then
echo "HTTP client leaked into the docx,spreadsheet,markdown,email feature set"
cargo test -p markkit --no-default-features --features docx,spreadsheet,markdown,email,odt
if cargo tree -p markkit --no-default-features --features docx,spreadsheet,markdown,email,odt -e normal | grep -E 'reqwest|hyper'; then
echo "HTTP client leaked into the docx,spreadsheet,markdown,email,odt feature set"
exit 1
fi
if cargo tree -p markkit --no-default-features --features docx,spreadsheet -e normal | grep -E 'tokio'; then
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- `markkit mcp`: Model Context Protocol server over stdio exposing the `markkit` tool
(`tools/list`, `tools/call`). URL fetching keeps the SSRF guard; `--no-url` keeps it offline,
the `extract` limit flags and `--max-output-chars` bound each call.
- ODT input (`odt` feature, in `all_formats`; `.odt`, OpenDocument text content type, sniffed from the
ODF `mimetype`): headings, Title style, bold/italic/strike, links, lists, tables, with the same
bounded ZIP/XML reader as DOCX. New `Format::Odt`, `Error::OdtExtract`.
- Email input (`email` feature, in `all_formats`; `.eml`, `message/rfc822`): subject as title, From /
To / Cc / Date, plain-text body (HTML alternative converted when there is no plain part) and an
attachment list. New `Format::Eml`, `Error::EmailExtract`, `ExtractionStats::attachments`, `WarningCode::EncodingProblem`.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package for browsers and edge runtimes.
- **HTML** – Web pages and HTML documents; nav, header, footer, scripts, styles and forms stripped by default
- **DOCX** – Headings, lists, tables, links, emphasis
- **PPTX** – One section per slide: titles, bullets, tables, links, speaker notes
- **ODT** – OpenDocument text: headings, lists, tables, links, emphasis
- **Spreadsheets** – XLSX/XLSM, XLSB, XLS and ODS workbooks to markdown tables, one section per sheet
- **Email (EML)** – Subject, sender, recipients, date, body (plain preferred, HTML converted), attachment list
- **Jupyter notebooks** – Markdown cells, code cells with language, text outputs and tracebacks
Expand All @@ -37,6 +38,7 @@ package for browsers and edge runtimes.
| HTML / XHTML | Supported | `html` (default) |
| DOCX | Supported | `docx` |
| PPTX, PPTM, PPSX | Supported | `pptx` |
| ODT | Supported | `odt` |
| XLSX, XLSM, XLSB, XLS, ODS | Supported | `spreadsheet` |
| Plain text | Supported | always on |
| CSV, TSV | Supported | always on |
Expand Down Expand Up @@ -205,7 +207,8 @@ let markkit = Markkit::builder()
| `pdf` | No | PDF text-layer extraction (hayro-syntax, pure Rust) |
| `markdown` | No | Markdown normalization (pulldown-cmark) |
| `email` | No | EML conversion (mail-parser) |
| `all_formats` | No | `html`, `pdf`, `docx`, `pptx`, `spreadsheet`, `markdown`, `email` |
| `odt` | No | ODT conversion (zip + quick-xml) |
| `all_formats` | No | `html`, `pdf`, `docx`, `pptx`, `spreadsheet`, `markdown`, `email`, `odt` |
| `tool` | No | `markkit::tool::MarkkitTool` LLM agent tool (Everruns toolkit contract) |
| `logging` | No | Reserved: pulls in `tracing`, emits no events yet |

Expand Down
1 change: 1 addition & 0 deletions crates/markkit-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pptx = ["markkit/pptx"]
spreadsheet = ["markkit/spreadsheet"]
markdown = ["markkit/markdown"]
email = ["markkit/email"]
odt = ["markkit/odt"]

[dependencies]
markkit = { path = "../markkit", version = "0.1.1", features = ["http_client", "tool"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/markkit-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub struct Warning {
pub struct Extraction {
pub markdown: String,
pub title: Option<String>,
/// Detected format (`html`, `docx`, `pptx`, `xlsx`, `xlsb`, `xls`, `ods`, `text`, `markdown`, `csv`, `tsv`, `ipynb`, `eml`).
/// Detected format (`html`, `docx`, `pptx`, `xlsx`, `xlsb`, `xls`, `ods`, `text`, `markdown`, `csv`, `tsv`, `ipynb`, `eml`, `odt`).
pub format: String,
pub warnings: Vec<Warning>,
pub stats: ExtractionStats,
Expand Down
3 changes: 2 additions & 1 deletion crates/markkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ pptx = ["dep:zip", "dep:quick-xml"]
spreadsheet = ["dep:calamine", "dep:zip", "dep:quick-xml", "dep:cfb"]
markdown = ["dep:pulldown-cmark"]
email = ["dep:mail-parser"]
odt = ["dep:zip", "dep:quick-xml"]
# Opt-in: the default build has no network stack (no reqwest/hyper/TLS).
http_client = ["dep:reqwest", "dep:tokio", "tokio/net"]
logging = ["dep:tracing"]
# LLM tool contract (`markkit::tool`): Everruns toolkit-library shape.
tool = ["dep:tower", "dep:futures-core", "dep:base64", "dep:tokio", "tokio/sync"]
# JSON Schema for the IR (`markkit::ir::json_schema()`)
json-schema = ["dep:schemars"]
all_formats = ["html", "pdf", "docx", "pptx", "spreadsheet", "markdown", "email"]
all_formats = ["html", "pdf", "docx", "pptx", "spreadsheet", "markdown", "email", "odt"]

[dependencies]
# Async (optional): only URL fetching and the tool's output channel need tokio.
Expand Down
2 changes: 2 additions & 0 deletions crates/markkit/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub enum Error {

#[error("PPTX extraction error: {0}")]
PptxExtract(String),
#[error("ODT extraction error: {0}")]
OdtExtract(String),

#[error("Spreadsheet extraction error: {0}")]
SpreadsheetExtract(String),
Expand Down
29 changes: 26 additions & 3 deletions crates/markkit/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ pub struct ExtractionStats {
pub sheets: Option<usize>,
/// Data rows rendered across all sheets / the CSV table (header rows excluded).
pub rows: Option<usize>,
/// Non-empty paragraphs, headings and list items (DOCX, plain text, Markdown).
/// Non-empty paragraphs, headings and list items (DOCX, ODT, plain text, Markdown).
pub paragraphs: Option<usize>,
/// Tables (DOCX, Markdown, CSV/TSV).
/// Tables (DOCX, ODT, Markdown, CSV/TSV).
pub tables: Option<usize>,
/// Non-empty cells rendered (Jupyter notebooks).
pub cells: Option<usize>,
Expand Down Expand Up @@ -321,7 +321,7 @@ impl Markkit {
.map(|e| e.markdown)
}

/// Convert a document (HTML, PDF, DOCX, PPTX, XLSX/XLSB/XLS/ODS, plain text, Markdown,
/// Convert a document (HTML, PDF, DOCX, PPTX, ODT, XLSX/XLSB/XLS/ODS, plain text, Markdown,
/// CSV/TSV, Jupyter notebooks, email) to Markdown with metadata.
///
/// The format is detected from `hint.content_type`, then the extension of
Expand Down Expand Up @@ -402,6 +402,7 @@ impl Markkit {
Format::Csv | Format::Tsv => self.parse_csv(bytes, format, budget),
Format::Notebook => self.parse_notebook(bytes, budget),
Format::Eml => self.parse_email(bytes, budget),
Format::Odt => self.parse_odt(bytes, budget),
Format::Doc => Err(Error::UnsupportedFormat(
"legacy Word 97-2003 (.doc) documents are not supported".into(),
)),
Expand Down Expand Up @@ -478,6 +479,28 @@ impl Markkit {
Ok(p)
}

#[cfg(feature = "odt")]
fn parse_odt(&self, bytes: &[u8], budget: &Budget) -> crate::Result<ParsedDocument> {
let out = guarded("ODT", Error::OdtExtract, || {
crate::odt::parse(bytes, &self.config, budget)
})?;
let mut doc = out.doc;
self.config.transforms.apply(&mut doc)?;
let mut p = ParsedDocument::new(doc, Format::Odt);
p.title = out.title;
p.warnings = out.warnings;
p.stats.paragraphs = Some(out.paragraphs);
p.stats.tables = Some(out.tables);
Ok(p)
}

#[cfg(not(feature = "odt"))]
fn parse_odt(&self, _bytes: &[u8], _budget: &Budget) -> crate::Result<ParsedDocument> {
Err(Error::UnsupportedFormat(
"ODT support requires the 'odt' feature".to_string(),
))
}

#[cfg(feature = "email")]
fn parse_email(&self, bytes: &[u8], budget: &Budget) -> crate::Result<ParsedDocument> {
let out = guarded("email", Error::EmailExtract, || {
Expand Down
13 changes: 12 additions & 1 deletion crates/markkit/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum Format {
Docx,
/// Legacy Word 97-2003 binary (`.doc`). Detected but not supported.
Doc,
/// OpenDocument text (`.odt`, `.ott`).
Odt,
/// Office Open XML spreadsheet (`.xlsx`, `.xlsm`, `.xltx`).
Xlsx,
/// Excel binary workbook (`.xlsb`).
Expand Down Expand Up @@ -82,6 +84,7 @@ impl<'a> DocumentHint<'a> {

const OLE2_MAGIC: &[u8] = b"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1";
const ODS_MIMETYPE: &[u8] = b"mimetypeapplication/vnd.oasis.opendocument.spreadsheet";
const ODT_MIMETYPE: &[u8] = b"mimetypeapplication/vnd.oasis.opendocument.text";

impl Format {
/// Short lowercase name (`"docx"`, `"xlsx"`, …).
Expand All @@ -91,6 +94,7 @@ impl Format {
Format::Pdf => "pdf",
Format::Docx => "docx",
Format::Doc => "doc",
Format::Odt => "odt",
Format::Xlsx => "xlsx",
Format::Xlsb => "xlsb",
Format::Xls => "xls",
Expand Down Expand Up @@ -125,6 +129,8 @@ impl Format {
Format::Xlsx
} else if ct.contains("ms-excel") {
Format::Xls
} else if ct.contains("opendocument.text") {
Format::Odt
} else if ct.contains("opendocument.spreadsheet") {
Format::Ods
} else if ct.contains("presentationml") || ct.contains("ms-powerpoint.") {
Expand Down Expand Up @@ -160,6 +166,7 @@ impl Format {
"xlsb" => Format::Xlsb,
"xls" | "xlt" => Format::Xls,
"ods" => Format::Ods,
"odt" | "ott" => Format::Odt,
"pptx" | "pptm" | "ppsx" | "ppsm" | "potx" | "potm" => Format::Pptx,
"ppt" | "pps" | "pot" => Format::Ppt,
"csv" => Format::Csv,
Expand Down Expand Up @@ -231,8 +238,11 @@ fn contains(haystack: &[u8], needle: &[u8]) -> bool {
fn sniff_zip(bytes: &[u8]) -> Format {
// ODF stores an uncompressed `mimetype` entry first; OOXML part names are
// stored uncompressed in local and central directory headers.
if contains(&bytes[..bytes.len().min(256)], ODS_MIMETYPE) {
let head = &bytes[..bytes.len().min(256)];
if contains(head, ODS_MIMETYPE) {
Format::Ods
} else if contains(head, ODT_MIMETYPE) {
Format::Odt
} else if contains(bytes, b"word/document") {
Format::Docx
} else if contains(bytes, b"xl/workbook.bin") {
Expand Down Expand Up @@ -293,6 +303,7 @@ mod tests {
Format::Tsv,
Format::Notebook,
Format::Eml,
Format::Odt,
Format::PlainText,
Format::Markdown,
Format::Unknown,
Expand Down
17 changes: 15 additions & 2 deletions crates/markkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
//! - **PPTX** (`pptx`): Convert PowerPoint presentations, one section per slide
//! - **Spreadsheet** (`spreadsheet`): Convert XLSX/XLSM, XLSB, XLS and ODS workbooks to tables
//! - **Plain text** (always on): `text/plain`, `.txt`
//! - **ODT** (`odt`): OpenDocument text documents
//! - **Email** (`email`): `.eml` / `message/rfc822`, headers, body and attachment list
//! - **Jupyter notebooks** (always on): `.ipynb`, cells and text outputs
//! - **CSV / TSV** (always on): `text/csv`, `text/tab-separated-values`, `.csv`, `.tsv`, one table
Expand All @@ -51,7 +52,12 @@
//! `Markkit::extract_url`. Office formats only, no HTML:
//! `markkit = { version = "0.1", default-features = false, features = ["docx", "spreadsheet"] }`.

#[cfg(any(feature = "docx", feature = "pptx", feature = "spreadsheet"))]
#[cfg(any(
feature = "docx",
feature = "pptx",
feature = "spreadsheet",
feature = "odt"
))]
mod archive;
pub mod config;
mod csv;
Expand All @@ -71,6 +77,8 @@ pub mod limits;
#[cfg(feature = "markdown")]
mod markdown;
mod notebook;
#[cfg(feature = "odt")]
mod odt;
#[cfg(any(feature = "docx", feature = "pptx"))]
mod ooxml;
#[cfg(feature = "pdf")]
Expand All @@ -86,7 +94,12 @@ mod text;
#[cfg(feature = "tool")]
pub mod tool;
pub mod transform;
#[cfg(any(feature = "docx", feature = "pptx", feature = "spreadsheet"))]
#[cfg(any(
feature = "docx",
feature = "pptx",
feature = "spreadsheet",
feature = "odt"
))]
#[cfg_attr(not(feature = "docx"), allow(dead_code))] // DOCX uses every helper
mod xml;

Expand Down
Loading
Loading