Convert PDF, Office, legacy Office, or RTF input into Markdown, with optional image extraction

MIT 2.0k downloads
Updated 17 days ago Repository
markdownpdfdocxdocxlspptrtfwasmdocument

Run

wippy run kickside/doc2md

Doc2MD

Doc2MD is a Wippy library module that converts documents to Markdown through a Rust WebAssembly component.

It supports PDF, modern Office files (.docx, .xlsx, .pptx), legacy Office files (.doc, .xls, .ppt), and RTF. Image extraction is opt-in, so the default result is text-focused Markdown close to classic document-to-markdown converters.

Public API

Import the Lua SDK from the public module entry:

imports:
  kickside.convert.doc2md: kickside.convert.doc2md:doc2md

Convert raw bytes:

local doc2md = require("doc2md")

local document, err = doc2md.convert({
    bytes = uploaded_bytes,
})
if err ~= nil then
    return nil, err
end

return document.markdown

Convert a URL:

local document, err = doc2md.convert({
    url = "https://example.com/report.pdf",
})

Enable image extraction explicitly (a second options argument):

local document, err = doc2md.convert({ bytes = uploaded_bytes }, {
    extract_images = true,
})

Request and options

doc2md.convert(request [, options]):

  • request — a table with exactly one source:
    • bytes: non-empty Lua binary string with the document content, or
    • url: HTTP(S) URL to download.
  • options — an optional table (unknown keys are rejected):
    • extract_images: optional boolean, default false.

doc2md.new(runtime) builds an instance with injected runtime dependencies ({ funcs?, http_client? }) for tests or a custom HTTP client. See RFC 0001.

Errors

Invalid input and conversion failures return nil, err where err is a structured table with message, kind, and retryable. kind is one of invalid_input, download_failed, decode_failed, internal. tostring(err) and string concatenation yield the message. Inputs are capped at 128 MiB; URL fetches are http(s)-only with a 30s timeout, a size cap, and runtime private-IP blocking.

Response

Successful conversions return:

{
    markdown = "# Converted markdown",
    images = {
        {
            name = "image1.png",
            data = raw_image_bytes,
        },
    },
}

images is always a table. It is empty when extract_images is false or when the document has no extractable images. Image data is returned as raw binary Lua strings, not base64.

Module Surface

The published doc2md namespace contains only the production entries:

  • kickside.convert.doc2md:doc2md: Lua SDK.
  • kickside.convert.doc2md:converter: WebAssembly converter function.
  • kickside.convert.doc2md:assets: embedded filesystem for doc2md.wasm.

Tests, fixtures, and load harnesses live under app.env:**. That namespace is excluded by wippy.yaml, so sample documents are not part of the published package.

Development

make install
make build-component
make lint
make test
make load-test

make build-component requires the Rust toolchain with the wasm32-wasip2 target. make load-test runs excluded local validation entries and does not change the publish surface.

Publish

Validate the exact publish path without uploading:

make publish-dry-run

Publish as a Wippy library module:

make publish

Those targets call wippy publish with --module-type library so first-time registration does not create the module as an application.

License

MIT.