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
36 changes: 34 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,49 @@ jobs:
done
cargo run -q -p markkit --features tool --example agent_tool

wasm:
# Browser package @everruns/markkit-wasm: build the wasm32-unknown-unknown
# bundle and run its headless Node tests. See knowledge/runtimes/wasm-package.md.
name: WASM web package
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4
with:
node-version: "24.22.1"
- name: Check core for wasm32 (no warnings)
run: cargo check -p markkit --no-default-features --features html,docx,spreadsheet,tool --target wasm32-unknown-unknown
- name: Install wasm-bindgen-cli (matches Cargo.lock)
run: bash scripts/install-wasm-bindgen.sh
- name: Build package
run: bash crates/markkit-wasm/scripts/build.sh release
- name: Integration tests (headless Node)
run: node --test crates/markkit-wasm/__test__/*.test.mjs
- name: Verify npm archive contents
run: |
cd crates/markkit-wasm/pkg
npm pack --dry-run --json --ignore-scripts > "$RUNNER_TEMP/wasm-pack.json"
node -e 'const files=new Set(require(process.argv[1])[0].files.map(f=>f.path)); for (const f of ["index.js","index.d.ts","markkit_wasm.js","markkit_wasm_bg.wasm","README.md"]) if(!files.has(f)) throw Error("archive lacks "+f)' "$RUNNER_TEMP/wasm-pack.json"

check:
name: Check
needs: [lint, audit, test]
needs: [lint, audit, test, wasm]
runs-on: ubuntu-latest
if: always()
steps:
- name: All jobs passed
run: |
if [[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.audit.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]]; then
[[ "${{ needs.test.result }}" != "success" ]] || \
[[ "${{ needs.wasm.result }}" != "success" ]]; then
echo "One or more jobs failed"
exit 1
fi
77 changes: 77 additions & 0 deletions .github/workflows/publish-wasm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Publish @everruns/markkit-wasm (browser / edge WebAssembly package) to npm.
#
# Run from the verified release tag, like publish-js.yml. Defaults to a dry run.
# Publishes with npm trusted publishing (GitHub OIDC), no stored token:
# npmjs.com @everruns/markkit-wasm > Settings > Trusted publisher:
# GitHub Actions, everruns/markkit, workflow publish-wasm.yml, no environment.
# npm >= 11.5.1 is required for the OIDC exchange (provenance is automatic).

name: Publish WASM

on:
workflow_dispatch:
inputs:
dry_run:
description: "Dry run"
required: false
default: "true"

permissions:
contents: read

jobs:
publish:
name: Publish npm package (wasm)
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
id-token: write # npm trusted publishing + provenance
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Verify publish source is reachable from main
run: |
git fetch --no-tags origin main:refs/remotes/origin/main
git merge-base --is-ancestor "$(git rev-parse HEAD)" origin/main
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: actions/setup-node@v4
with:
node-version: "24.22.1"
registry-url: "https://registry.npmjs.org"
- name: Require npm with trusted publishing (>= 11.5.1)
run: |
npm_ok() {
node -e 'const [a,b,c]=process.argv[1].split(".").map(Number);process.exit(a>11||(a==11&&(b>5||(b==5&&c>=1)))?0:1)' "$(npm --version)"
}
npm_ok || npm install -g npm@11.6.2
npm_ok || { echo "::error::npm $(npm --version) lacks trusted publishing"; exit 1; }
- name: Install wasm-bindgen-cli (matches Cargo.lock)
run: bash scripts/install-wasm-bindgen.sh
- name: Build package
run: bash crates/markkit-wasm/scripts/build.sh release
- name: Verify build preserved the lockfile
run: git diff --exit-code -- Cargo.lock
- name: Integration tests (headless Node)
run: node --test crates/markkit-wasm/__test__/*.test.mjs
- name: Verify tag matches package version
run: |
PKG_VERSION=$(node -p "require('./crates/markkit-wasm/pkg/package.json').version")
if [[ "$GITHUB_REF" =~ ^refs/tags/v ]] && [ "${GITHUB_REF#refs/tags/v}" != "$PKG_VERSION" ]; then
echo "::error::tag ${GITHUB_REF#refs/tags/} does not match package version $PKG_VERSION"
exit 1
fi
- name: Publish
working-directory: crates/markkit-wasm/pkg
env:
DRY_RUN: ${{ inputs.dry_run }}
run: |
if [ "$DRY_RUN" != "false" ]; then
npm publish --dry-run --ignore-scripts --access public
else
npm publish --ignore-scripts --access public
fi
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Fix root cause. Unsure: read more code; if stuck, ask w/ short options. Unrecogn
| 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 |
| runtimes/wasm-package | wasm-bindgen `@everruns/markkit-wasm`, browser/edge, build + tests |
| runtimes/node-package | NAPI-RS `@everruns/markkit`, npm, binding parity table |
| operations/testing | Test layout, categories, CI jobs |
| operations/documentation | Doc trees and what each must cover |
Expand All @@ -44,7 +45,7 @@ Fix root cause. Unsure: read more code; if stuck, ask w/ short options. Unrecogn

- User docs: `README.md`, `docs/`
- API reference: rustdoc in `crates/markkit/src/lib.rs` and modules
- Binding READMEs (`crates/markkit-python/README.md`, `crates/markkit-js/README.md`) ship to PyPI/npm, keep them in sync with exports
- Binding READMEs (`crates/markkit-python/README.md`, `crates/markkit-js/README.md`, `crates/markkit-wasm/README.md`) ship to PyPI/npm, keep them in sync with exports
- Durable engineering knowledge: `knowledge/`, never `docs/`

### Shipping
Expand Down Expand Up @@ -86,6 +87,7 @@ just pre-pr # Pre-PR checks
| `markkit-eval` | LLM evaluation harness (stub) |
| `markkit-python` | Python bindings (PyO3), module `markkit` |
| `markkit-js` | Node.js bindings (NAPI-RS), package `@everruns/markkit` |
| `markkit-wasm` | Browser/edge WebAssembly (wasm-bindgen), package `@everruns/markkit-wasm` |

### Rust

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
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`.
- `@everruns/markkit-wasm`: browser / edge WebAssembly package (`crates/markkit-wasm`), single-threaded,
no special headers. `Markkit` class (HTML, DOCX, XLSX/XLS/ODS with validated camelCase options) and
`MarkkitTool`. Built by `crates/markkit-wasm/scripts/build.sh`, published by `publish-wasm.yml`.
- `Markkit::extract_url_document`: `extract_url` with format, title, warnings and stats.

### Security
Expand Down
24 changes: 24 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"crates/markkit-eval",
"crates/markkit-python",
"crates/markkit-js",
"crates/markkit-wasm",
]

[workspace.package]
Expand Down Expand Up @@ -76,3 +77,8 @@ lto = "thin"
codegen-units = 1
panic = "abort"
strip = true

# Browser bundle (crates/markkit-wasm/scripts/build.sh): size over speed.
[profile.wasm-release]
inherits = "release"
opt-level = "z"
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Extract clean markdown from any format. Written in Rust.

Markkit converts HTML, Word documents and spreadsheets into well-structured
markdown, built for feeding untrusted documents to LLMs and agents. It ships as
a Rust library, a CLI, and native bindings for Python and Node.js.
a Rust library, a CLI, native bindings for Python and Node.js, and a WebAssembly
package for browsers and edge runtimes.

## Features

Expand Down Expand Up @@ -101,6 +102,15 @@ const fromHtml = await extractHtml('<h1>Hello</h1>');
const fromUrl = await extractUrl('https://example.com');
```

### Browser / edge (WebAssembly)

```javascript
import { initMarkkit, Markkit } from '@everruns/markkit-wasm';

await initMarkkit();
const md = new Markkit().extractHtml('<h1>Hello</h1>');
```

## Installation

### Rust
Expand Down Expand Up @@ -132,6 +142,16 @@ npm install @everruns/markkit

Native binary: Linux x64 (glibc).

### Browser / edge (WebAssembly)

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

Any JS runtime (browsers, Cloudflare Workers, Deno, Bun, Node); no special headers.
HTML, DOCX and spreadsheets; no URL fetching. See
[`crates/markkit-wasm/README.md`](crates/markkit-wasm/README.md).

## Configuration

```rust
Expand Down
4 changes: 4 additions & 0 deletions crates/markkit-wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# wasm-bindgen build output (scripts/build.sh)
/pkg/
node_modules/
*.tgz
38 changes: 38 additions & 0 deletions crates/markkit-wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Markkit browser bindings via wasm-bindgen.
#
# Decision: slim, single-threaded wasm32-unknown-unknown build, separate from the
# napi `markkit-js` crate (same split as bashkit-wasm). No threads, no WASI, no
# SharedArrayBuffer and no COOP/COEP headers, so it runs in browsers, edge
# workers (Cloudflare, Vercel, Deno Deploy), Node, Deno and Bun.
#
# Feature surface: html, docx, spreadsheet, tool. `http_client` is excluded:
# reqwest needs sockets; JS hosts fetch themselves and pass bytes in, which also
# keeps SSRF policy with the host. See knowledge/runtimes/wasm-package.md.

[package]
name = "markkit-wasm"
description = "Browser (WebAssembly) bindings for Markkit markdown extraction"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
publish = false

[lib]
crate-type = ["cdylib", "rlib"]
doc = false

[dependencies]
markkit = { path = "../markkit", default-features = false, features = [
"html",
"docx",
"spreadsheet",
"tool",
] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
serde = { workspace = true }
serde_json = { workspace = true }
serde-wasm-bindgen = "0.6"
File renamed without changes.
Loading
Loading