Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ jobs:
for ex in basic custom_selectors configuration ir_access; do
cargo run -q -p markkit --example "$ex"
done
cargo run -q -p markkit --features tool --example agent_tool

check:
name: Check
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Fix root cause. Unsure: read more code; if stuck, ask w/ short options. Unrecogn
| foundations/ir | Canonical IR between parsers and the markdown renderer |
| formats/html-extraction | HTML element mapping, stripping, whitespace, depth limits |
| formats/format-support | Formats, bytes-in API, detection order, DOCX + spreadsheet mapping |
| integrations/tool-contract | `MarkkitTool` LLM tool (`tool` feature), Everruns toolkit contract |
| security/security-model | One-page threat/invariant summary |
| security/threat-model | Threats T1-T6, mitigations, test map, resource limits, invariants S1-S12 |
| runtimes/python-package | PyO3 `markkit` module, maturin, PyPI |
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
`<figure>`/`<figcaption>` and table spans/alignment. Row-header `<th>` cells in table bodies are
no longer dropped.
- `Document.ir_version` (`markkit::ir::IR_VERSION = 2`) is serialized with the IR JSON.
- `tool` feature: `markkit::tool::MarkkitTool`, an LLM agent tool following the Everruns
toolkit-library contract (builder, OpenAI tool definition, input/output schemas, `tower::Service`,
single-use `ToolExecution`, `en-US`/`uk-UA` text). One call converts a `url`, `html` or base64
document to Markdown, with output capped by `max_output_chars`. Example: `examples/agent_tool.rs`.
- `Markkit::extract_url_document`: `extract_url` with format, title, warnings and stats.

### Security

Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ cfb = "0.15" # XLS (OLE2) container pre-scan
# Logging
tracing = "0.1"

# LLM tool contract (markkit `tool` feature)
tower = { version = "0.5", default-features = false, features = ["util"] }
futures-core = "0.3"
base64 = "0.23"

# Python bindings
pyo3 = { version = "0.30" }
pyo3-async-runtimes = { version = "0.30", features = ["tokio-runtime"] }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ let markkit = Markkit::builder()
| `spreadsheet` | No | XLSX/XLSM, XLS, ODS conversion (calamine) |
| `pdf` | No | Placeholder; PDF currently returns `UnsupportedFormat` |
| `all_formats` | No | `html`, `pdf`, `docx`, `spreadsheet` |
| `tool` | No | `markkit::tool::MarkkitTool` LLM agent tool (Everruns toolkit contract) |
| `logging` | No | Reserved: pulls in `tracing`, emits no events yet |

`http_client` is optional. Embedders that fetch content themselves can build without
Expand Down
67 changes: 67 additions & 0 deletions crates/markkit-wasm/pkg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# @everruns/markkit-wasm

Markkit compiled to WebAssembly: extract clean Markdown from HTML, DOCX and
spreadsheets (XLSX/XLSM, XLS, ODS) in the **browser and any JavaScript runtime**:
edge workers (Cloudflare Workers, Vercel Edge, Deno Deploy), Node, Deno and Bun.

Single-threaded `wasm-bindgen` build: no `SharedArrayBuffer`, no `COOP`/`COEP`
headers. For a native Node.js addon use
[`@everruns/markkit`](https://www.npmjs.com/package/@everruns/markkit).

No URL fetching: the host fetches and passes bytes in, so network policy stays
with the host.

## Install

```bash
npm install @everruns/markkit-wasm
```

## Quick start

```js
import { initMarkkit, Markkit } from "@everruns/markkit-wasm";

await initMarkkit(); // once

const mk = new Markkit({ includeSelectors: ["article"], preserveImages: false });
console.log(mk.extractHtml("<article><h1>Hello</h1></article>")); // # Hello

const res = await fetch("/report.docx");
const doc = mk.extractDocument(new Uint8Array(await res.arrayBuffer()), {
contentType: res.headers.get("content-type") ?? undefined,
fileName: "report.docx",
});
console.log(doc.title, doc.format, doc.markdown);
```

## Options

`maxInputSize`, `preserveLinks`, `preserveImages`, `includeSelectors`,
`excludeSelectors` (added to the defaults), `maxArchiveEntries`,
`maxArchiveEntrySize`, `maxArchiveTotalSize`, `maxSheetRows`, `maxSheetCols`,
`maxSpreadsheetCells`, `includeHiddenSheets`. Unknown keys throw.

## Agent tool

`MarkkitTool` follows the Everruns toolkit contract: tool definition, schemas,
system prompt and a validated `execute`. Sources are `html` or `content_base64`.

```js
import { initMarkkit, MarkkitTool } from "@everruns/markkit-wasm";
await initMarkkit();

const tool = new MarkkitTool({ maxOutputChars: 20000 });
const tools = [tool.toolDefinition()]; // send with the model request

try {
const result = await tool.execute({ html: "<h1>Title</h1>" });
// { markdown, title, format, warnings, truncated }
} catch (e) {
if (e.userFacing) { /* safe to return e.message to the model */ }
}
```

## License

MIT
106 changes: 106 additions & 0 deletions crates/markkit-wasm/pkg/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/** Load the WebAssembly module. Call (and await) once before using any class. */
export function initMarkkit(
input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module,
): Promise<void>;

/** Library version. */
export function version(): string;

/** Extraction options (all optional; defaults match the Rust builder). */
export interface MarkkitOptions {
/** Maximum input size in bytes (default 50 MiB). */
maxInputSize?: number;
preserveLinks?: boolean;
preserveImages?: boolean;
/** CSS selectors to include (empty = whole document). */
includeSelectors?: string[];
/** CSS selectors to drop, added to nav/footer/header/script/style/noscript. */
excludeSelectors?: string[];
maxArchiveEntries?: number;
maxArchiveEntrySize?: number;
maxArchiveTotalSize?: number;
maxSheetRows?: number;
maxSheetCols?: number;
maxSpreadsheetCells?: number;
includeHiddenSheets?: boolean;
}

export interface DocumentHint {
/** MIME type, e.g. from a `Content-Type` header. */
contentType?: string;
/** File name or URL path; only the extension is used. */
fileName?: string;
}

export interface ExtractionStats {
pages: number | null;
sheets: number | null;
rows: number | null;
paragraphs: number | null;
tables: number | null;
}

export interface Extraction {
markdown: string;
title: string | null;
format: string;
warnings: string[];
stats: ExtractionStats;
}

export class Markkit {
constructor(options?: MarkkitOptions);
extractHtml(html: string): string;
extractDocument(bytes: Uint8Array, hint?: DocumentHint): Extraction;
extractBytes(bytes: Uint8Array, contentType?: string): string;
free(): void;
}

export interface MarkkitToolOptions extends MarkkitOptions {
/** `en-US` (default) or `uk-UA`. */
locale?: string;
/** Cap on returned Markdown characters (default 100 000). */
maxOutputChars?: number;
}

/** Tool call arguments (exactly one of `html` / `content_base64`). */
export interface MarkkitToolArgs {
html?: string;
content_base64?: string;
file_name?: string;
content_type?: string;
max_chars?: number | null;
}

export interface MarkkitToolResult {
markdown: string;
title: string | null;
format: string;
warnings: string[];
truncated: boolean;
}

/** Rejection reason of `MarkkitTool.execute`. */
export interface MarkkitToolError extends Error {
/** True when the message is safe to return to the model. */
userFacing: boolean;
}

/** LLM agent tool (Everruns toolkit contract). No `url` source in wasm. */
export class MarkkitTool {
constructor(options?: MarkkitToolOptions);
readonly name: string;
readonly displayName: string;
readonly shortDescription: string;
readonly description: string;
readonly version: string;
readonly locale: string;
help(): string;
systemPrompt(): string;
inputSchema(): Record<string, unknown>;
outputSchema(): Record<string, unknown>;
/** OpenAI-compatible function tool definition. */
toolDefinition(): Record<string, unknown>;
execute(args: MarkkitToolArgs): Promise<MarkkitToolResult>;
free(): void;
}
24 changes: 24 additions & 0 deletions crates/markkit-wasm/pkg/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Entry point for @everruns/markkit-wasm.
//
// `initMarkkit()` loads the .wasm once. In browsers and edge runtimes the
// generated glue fetches `markkit_wasm_bg.wasm` relative to this module; in
// Node (no fetch for file: URLs) the bytes are read from disk instead.
import init, { Markkit, MarkkitTool, version } from "./markkit_wasm.js";

let ready;

export function initMarkkit(input) {
if (!ready) {
ready = (async () => {
let source = input;
if (source === undefined && typeof process !== "undefined" && process.versions?.node) {
const { readFile } = await import("node:fs/promises");
source = await readFile(new URL("./markkit_wasm_bg.wasm", import.meta.url));
}
await init(source === undefined ? undefined : { module_or_path: source });
})();
}
return ready;
}

export { Markkit, MarkkitTool, version };
119 changes: 119 additions & 0 deletions crates/markkit-wasm/pkg/markkit_wasm.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* tslint:disable */
/* eslint-disable */

/**
* Configured extractor.
*/
export class Markkit {
free(): void;
[Symbol.dispose](): void;
/**
* Document bytes to Markdown only.
*/
extractBytes(bytes: Uint8Array, content_type?: string | null): string;
/**
* Document bytes to `{ markdown, title, format, warnings, stats }`.
* `hint` is `{ contentType?, fileName? }`.
*/
extractDocument(bytes: Uint8Array, hint: any): any;
/**
* HTML string to Markdown.
*/
extractHtml(html: string): string;
/**
* `new Markkit(options?)`.
*/
constructor(options: any);
}

/**
* LLM agent tool (Everruns toolkit contract). Accepts `html` or `content_base64`.
*/
export class MarkkitTool {
free(): void;
[Symbol.dispose](): void;
/**
* Run one call. Resolves to `ToolOutput.result`; rejects with an `Error`
* whose `userFacing` property says whether the message is safe for the model.
*/
execute(args: any): Promise<any>;
help(): string;
inputSchema(): any;
/**
* `new MarkkitTool({ locale?, maxOutputChars?, ...extraction options })`.
*/
constructor(options: any);
outputSchema(): any;
systemPrompt(): string;
/**
* OpenAI-compatible function tool definition.
*/
toolDefinition(): any;
readonly description: string;
readonly displayName: string;
readonly locale: string;
readonly name: string;
readonly shortDescription: string;
readonly version: string;
}

/**
* Library version.
*/
export function version(): string;

export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;

export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly __wbg_markkit_free: (a: number, b: number) => void;
readonly __wbg_markkittool_free: (a: number, b: number) => void;
readonly markkit_extractBytes: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
readonly markkit_extractDocument: (a: number, b: number, c: number, d: number, e: number) => void;
readonly markkit_extractHtml: (a: number, b: number, c: number, d: number) => void;
readonly markkit_new: (a: number, b: number) => void;
readonly markkittool_description: (a: number, b: number) => void;
readonly markkittool_displayName: (a: number, b: number) => void;
readonly markkittool_execute: (a: number, b: number) => number;
readonly markkittool_help: (a: number, b: number) => void;
readonly markkittool_inputSchema: (a: number, b: number) => void;
readonly markkittool_locale: (a: number, b: number) => void;
readonly markkittool_name: (a: number, b: number) => void;
readonly markkittool_new: (a: number, b: number) => void;
readonly markkittool_outputSchema: (a: number, b: number) => void;
readonly markkittool_shortDescription: (a: number, b: number) => void;
readonly markkittool_systemPrompt: (a: number, b: number) => void;
readonly markkittool_toolDefinition: (a: number, b: number) => void;
readonly markkittool_version: (a: number, b: number) => void;
readonly version: (a: number) => void;
readonly __wasm_bindgen_func_elem_1938: (a: number, b: number, c: number, d: number) => void;
readonly __wasm_bindgen_func_elem_1940: (a: number, b: number, c: number, d: number) => void;
readonly __wbindgen_export: (a: number, b: number) => number;
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export3: (a: number) => void;
readonly __wbindgen_export4: (a: number, b: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_export5: (a: number, b: number, c: number) => void;
}

export type SyncInitInput = BufferSource | WebAssembly.Module;

/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;

/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
Loading
Loading