diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 47a945b..95411b3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -50,6 +50,15 @@ "strict": false, "description": "Use this skill when creating, reviewing, or editing Agent Skills-format skills, or when implementing skill discovery and loading in an agent client. It covers directory structure, SKILL.md metadata, progressive disclosure, evals, and repository conventions. Do not use this skill for general software work that does not involve the Agent Skills format or lifecycle." }, + { + "name": "anydoc", + "source": "./", + "skills": [ + "./anydoc" + ], + "strict": false, + "description": "Convert Word (.doc/.docx/.docm), PowerPoint (.ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm), Excel (.xls/.xlsx/.xlsm/.xlsb), OpenDocument (.odt/.ods/.odp), RTF, EPUB, CSV, and PDF documents to clean GitHub-Flavored Markdown locally with the Any Doc CLI (npx -y @firecrawl/anydoc@0.1.6): headings, GFM tables, slide structure, and footnotes in one pass. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for generating, editing, or validating documents (use documents), for ebook packaging (use epub), or for OCR of scanned or image-only PDFs (anydoc does not OCR; route to OCR tooling)." + }, { "name": "api-design-and-evolution", "source": "./", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 124e53f..32998bc 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -22,6 +22,7 @@ "./agent-council", "./agent-evals-and-observability", "./agent-skills", + "./anydoc", "./api-design-and-evolution", "./artifact-pyramids", "./autogen", diff --git a/README.md b/README.md index 720c9e8..80dcd96 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Operate an evaluated agent with tools and authority in production through a runt Reference for the Agent Skills open format itself — directory structure, frontmatter schema, naming conventions, and progressive disclosure model. Use this meta-skill when creating or reviewing any other skill in this repository. +### [anydoc](anydoc/SKILL.md) + +Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and text-based PDF documents to GitHub-Flavored Markdown locally with the Any Doc CLI. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for generating, editing, or validating documents, for ebook packaging, or for OCR of scanned or image-only PDFs. + ### [api-design-and-evolution](api-design-and-evolution/SKILL.md) Design, document, review, and evolve consumer-facing HTTP, GraphQL, RPC, event, webhook, and streaming interfaces. Covers consumer jobs, domain semantics, contracts, failure behavior, compatibility, deprecation, migration, and deployed-boundary verification. diff --git a/anydoc/README.md b/anydoc/README.md new file mode 100644 index 0000000..9265a07 --- /dev/null +++ b/anydoc/README.md @@ -0,0 +1,61 @@ +# anydoc — office documents to GitHub-Flavored Markdown + +Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF files into clean, LLM-friendly GitHub-Flavored Markdown — entirely on your own machine, with no API keys and no file uploads. One command turns a report, spreadsheet, or slide deck into markdown you (or an agent) can read, summarize, quote, and feed into a knowledge base. + +## Why Install This Skill + +Office documents are opaque to agents. A `.docx` or `.pptx` is a binary zip; a `.xls` is an OLE container; a PDF can be anything. Reading them directly means parsing formats, handling encodings, and reconstructing structure by hand — exactly the work anydoc automates. This skill gives your agent a single, verified command that converts all 8 format families (21 extensions) into GitHub-Flavored Markdown with headings, GFM tables, slide structure, and footnotes preserved, plus the knowledge of exactly where fidelity is lost (Excel number formats, legacy PowerPoint tables, PDF tables). + +The skill wraps the pinned `@firecrawl/anydoc` v0.1.6 CLI with a small helper script that adds input checks, friendly error hints for the known failure classes (scanned PDFs, encrypted files, malformed archives), batch conversion, dry-run planning, and JSON output — so an agent gets predictable exit codes and messages instead of guessing. It also documents the exact error vocabulary of the real CLI, so failures like "PDF has no extractable text ... OCR is required" are recognized and routed correctly (to OCR tooling) rather than retried blindly. + +## What You Get + +| Directory / file | What it provides | +| --- | --- | +| `SKILL.md` + `README.md` | The skill index (trigger, command map, verification steps) and this human-facing guide | +| `scripts/` | `anydoc` — an executable Python 3 wrapper with `convert` (single file or stdin, `-o` output), `batch` (many files, per-file status, summary), and `info` (tool + pinned CLI version), plus global `--json` and `--dry-run` | +| `references/` | Five focused guides: `formats.md` (what GFM each format produces, with fidelity caveats), `cli-reference.md` (verbatim `--help`, every flag, stdout/stderr conventions), `errors.md` (exit codes and the exact error messages), `workflows.md` (recipes: single conversion, batch, vault ingestion, piping, output verification), `sources.md` (upstream URLs, fixture provenance, verification procedure) | +| `tests/` | Unit tests for the wrapper (argparse, pre-validation, hints, dry-run, JSON, batch) — runnable offline | +| `evals/` | An eval manifest with fixture-backed cases covering docx→headings, xlsx→tables, pptx→slide structure, csv→table, legacy `.doc`, ODS preserved values, ODT, and the image-only-PDF OCR failure | +| `fixtures/` | 24 tiny sample documents (all < 5 MB): valid samples for every family plus error cases (image-only PDF, encrypted ODT, empty DOCX, unsupported extension) — used by the tests, evals, and recipes | + +## Quick Start + +You need Node.js 20+ and `npx` (no other install — the CLI and its native binary are fetched on first use): + +```bash +cd anydoc +npx -y @firecrawl/anydoc@0.1.6 fixtures/fixture-handmade-outline.docx +``` + +This converts the sample Word document and prints GitHub-Flavored Markdown to stdout (note the `#`/`##`/`###` heading lines). To write to a file instead: + +```bash +npx -y @firecrawl/anydoc@0.1.6 fixtures/fixture-handmade-outline.docx -o outline.md +``` + +Or use the wrapper for the same job: + +```bash +python3 scripts/anydoc convert fixtures/fixture-handmade-outline.docx -o outline.md +``` + +## Triggers + +Load this skill when the task involves any of these: + +- "Convert this Word/Excel/PowerPoint/PDF/EPUB/CSV file to markdown" +- "Extract the headings, tables, or slide content from this document" +- "Summarize this report / spreadsheet / deck" +- "Turn this CSV into a markdown table" +- "Read this document into markdown for a knowledge base or vault" +- "Convert this PDF to markdown" — but only for text-based PDFs; scanned or image-only PDFs fail (anydoc does not OCR) + +Do **not** load this skill for document generation or editing ("create a docx report", "build a PDF proposal", "validate this document") — that is the `documents` skill's job — or for EPUB authoring (`epub` skill). + +## Requirements + +- **Node.js >= 20** and `npx` (the CLI is distributed via npm; the native binary ships as a platform-specific npm `optionalDependency`, so there is no manual install or compilation). +- **Network once** — the first `npx` run downloads the package and binary; later runs use the npm cache. For permanent or fully offline use, run `npm install -g @firecrawl/anydoc` once. +- **Python 3** (standard library only) if you use the `scripts/anydoc` wrapper. +- **No API keys, no services** — conversion happens locally; files never leave your machine. diff --git a/anydoc/SKILL.md b/anydoc/SKILL.md new file mode 100644 index 0000000..686fa8d --- /dev/null +++ b/anydoc/SKILL.md @@ -0,0 +1,185 @@ +--- +name: anydoc +description: >- + Convert Word (.doc/.docx/.docm), PowerPoint (.ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm), + Excel (.xls/.xlsx/.xlsm/.xlsb), OpenDocument (.odt/.ods/.odp), RTF, EPUB, CSV, and + PDF documents to clean GitHub-Flavored Markdown locally with the Any Doc CLI + (npx -y @firecrawl/anydoc@0.1.6): headings, GFM tables, slide structure, and + footnotes in one pass. Use when a task needs the contents of an office document, + spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for + generating, editing, or validating documents (use documents), for ebook packaging + (use epub), or for OCR of scanned or image-only PDFs (anydoc does not OCR; route + to OCR tooling). +license: MIT +compatibility: >- + Node.js >= 20 and npx. The pinned CLI is @firecrawl/anydoc@0.1.6; the native + binary ships via npm optionalDependencies (no install step, no postinstall, no + compilation). Conversion runs entirely on your machine — no services, no API + keys, no uploads. The first npx run downloads the package once (network + required); later runs use the npm cache. +metadata: + skills: anydoc, markdown, conversion, docx, xlsx, pptx, pdf, odt, ods, odp, rtf, epub, csv, office, documents, firecrawl + tags: conversion, markdown, office, documents + source: https://github.com/firecrawl/anydoc +allowed-tools: Bash Read +--- + +# Any Doc — office documents to GitHub-Flavored Markdown + +The `anydoc` skill converts office documents, spreadsheets, presentations, +ebooks, CSV, and text-based PDFs into GitHub-Flavored Markdown using the pinned +Any Doc CLI (`@firecrawl/anydoc` v0.1.6). One shared document model and one GFM +serializer produce the same logical output across formats, and conversion runs +locally in milliseconds — no service, no API key, no file upload. + +## Overview + +Load this skill when a task needs the *contents* of a document the agent cannot +read directly: a Word report to summarize, a spreadsheet to turn into a table, +a slide deck to extract, a CSV to analyze, or an ebook or PDF to quote from. + +The skill ships a small Python helper (`scripts/anydoc`) that wraps the pinned +CLI and adds input pre-validation, friendly error hints, batch conversion, and +`--dry-run`/`--json` output. Every recipe in [references/workflows.md](references/workflows.md) +also shows the raw `npx` invocation, so the skill works with or without the +helper. + +## When to use + +- **Convert a document to markdown** — Word, PowerPoint, Excel, OpenDocument, + RTF, EPUB, CSV, or text-based PDF. +- **Extract structure** — headings, GFM tables, slide titles, speaker notes + (as blockquotes), and footnotes. +- **Feed documents to an LLM** — one-pass conversion to clean markdown for + summarization, extraction, or retrieval ingestion. +- **Batch a folder** — convert a directory of mixed office files for a vault + or knowledge base. +- **Read a document from stdin** — pipe bytes into `anydoc -`. + +## Format coverage (summary) + +anydoc covers **8 format families / 21 extensions** through **12 canonical +parsers**. The canonical formats are `doc, docx, odt, pdf, ppt, pptx, rtf, +epub, xlsx, ods, odp, csv`; extension aliases map through them (`.docm`→docx, +`.xls`→xlsx, `.pptm`→pptx, and so on). + +| Family | Extensions | Expected GFM output | +| --- | --- | --- | +| Word | `.doc` `.docx` `.docm` | `#`–`######` headings, GFM tables, `[^n]` footnotes | +| PowerPoint | `.ppt` `.pps` `.pot` `.pptx` `.pptm` `.ppsx` `.ppsm` | slide titles as plain paragraphs, bullet lists, speaker notes as `>` blockquotes, GFM tables (PPTX/ODP; legacy `.ppt` flattens tables to text lines) | +| Excel | `.xls` `.xlsx` `.xlsm` `.xlsb` | `## ` heading + one GFM table per worksheet; number formats dropped (raw cell values) | +| OpenDocument | `.odt` `.ods` `.odp` | same document/slide shapes as DOCX/PPTX; ODS keeps formatted display values | +| Rich Text Format | `.rtf` | same document shape as DOCX/ODT | +| EPUB | `.epub` | `#` chapter headings, GFM tables, internal anchor links | +| CSV | `.csv` | one GFM table; label-like first row promoted to header; delimiter sniffing; UTF-16 with BOM | +| PDF | `.pdf` | headings + inline emphasis, but a lower-fidelity pipeline: tables flatten to text, footnotes and links degrade. **Scanned or image-only PDFs fail** — anydoc does not OCR | + +See [references/formats.md](references/formats.md) for the full per-format +expectations and fidelity caveats, and [references/errors.md](references/errors.md) +for the exact failure messages (including the no-OCR error). + +## Command Map + +Commands are shown relative to the repository root. `` is any document +path (for example `anydoc/fixtures/fixture-handmade-outline.docx`); `-` reads +the document from stdin. + +| Need | Command | +| --- | --- | +| Convert one file (markdown to stdout) | `anydoc/scripts/anydoc convert ` | +| Convert one file to a markdown file | `anydoc/scripts/anydoc convert -o out.md` | +| Convert many files to a directory | `anydoc/scripts/anydoc batch ... --out-dir out/` | +| Show the tool and pinned CLI version | `anydoc/scripts/anydoc info` | +| Raw pinned CLI, one document | `npx -y @firecrawl/anydoc@0.1.6 [-o out.md]` | +| Raw pinned CLI, read stdin | `cat data.csv \| npx -y @firecrawl/anydoc@0.1.6 - --format csv` | + +Notes: + +- `scripts/anydoc` is an executable Python 3 script (shebang `#!/usr/bin/env + python3`); `python3 anydoc/scripts/anydoc ...` is equivalent when the + executable bit is unavailable. +- The raw `npx -y @firecrawl/anydoc@0.1.6` rows are the ground truth for + conversion behavior; the wrapper delegates to exactly that command. +- Always pin `@0.1.6` for reproducible conversions. `-y` answers npx's + "Ok to proceed?" prompt non-interactively — the CLI itself never prompts. +- Both forms share the same contract: one document per invocation, exit code + `0` success / `1` conversion or IO failure / `2` usage error, diagnostics as + exactly one `anydoc: ` line on stderr, and no prompts. + +## Reference Routing + +Load these on demand — one per topic: + +- [references/formats.md](references/formats.md) — the 8 families / 21 + extensions / 12 parsers, what GFM each format produces, and the fidelity + caveats (xlsx/xls number-format drop vs ODS preserved display values, legacy + `.ppt` table flattening, PDF lower-fidelity pipeline, merged-cell covered + spans, ODP same-serializer). +- [references/cli-reference.md](references/cli-reference.md) — verbatim + `--help`, every flag (`-o`, `-f`, `-h`, `-V`, `--format=x`, `--`), stdin via + `-`, stdout/stderr conventions including EPIPE, version pinning, Node >= 20, + and first-run/offline network behavior. +- [references/errors.md](references/errors.md) — exit codes 0/1/2, the verbatim + error-message vocabulary (io, unsupported, malformed, encrypted, EISDIR, + resource-limit, usage errors), the no-OCR caveat, and troubleshooting recipes. +- [references/workflows.md](references/workflows.md) — single conversion, + batch loops, vault ingestion, stdin/stdout piping, output verification, + large-file/resource-limit behavior, and startup cost. +- [references/sources.md](references/sources.md) — upstream URLs, access dates, + fixture provenance, and how every documented claim was verified against the + real CLI. + +## When not to use + +- **Generating, editing, or validating documents** — anydoc only converts + existing documents *to markdown*; it never creates, edits, or checks + documents. Use the `documents` skill for generation, inspection, and + validation of PDF/Word/Excel/PowerPoint artifacts. +- **Ebook packaging or EPUB authoring** — use the `epub` skill. anydoc reads + EPUBs to markdown but never writes or validates EPUB containers. +- **Scanned or image-only PDFs (OCR)** — anydoc does not perform OCR. Such + PDFs fail as `unsupported` with the OCR message; route the file to OCR + tooling or the hosted Firecrawl Parse API instead of retrying locally. Do + not claim local OCR support. +- **HTML and other web content** — HTML is not a supported input format; use a + web-scraping skill instead. +- **Binary media (images, video, audio)** — embedded images render as alt text + only; anydoc cannot transcribe media content. +- **Layout or rendering work** — output is GitHub-Flavored Markdown only; there + is no pagination, font, or template control. +- **Password-protected files** — encrypted documents fail with + `anydoc: document is encrypted`; there is no password or decryption option. + +## Verification + +Confirm a conversion before reporting it as done: + +1. **Check the exit code.** `0` means the CLI produced markdown. `1` means the + document could not be read or converted — read the single `anydoc: ` + stderr line and match it against [references/errors.md](references/errors.md). + `2` means the command itself was a usage error (bad flag, missing input, + invalid `--format`). +2. **Check the output shape.** The markdown must contain the structural markers + your format actually produces: + - Word / ODT / RTF / text-based PDF: `#`/`##` headings. For PDF, do not + expect GFM tables or `[^1]:` footnote definitions — that pipeline + flattens them. + - Spreadsheets (xlsx/xls/ods) and CSV: `|`-delimited GFM tables. xlsx/xls + show raw cell values (`0.155`, `1234.5`); ODS shows formatted display + values (`15.5%`, `$1,234.50`). + - Presentations (pptx/odp): slide titles as plain paragraphs, `>` + blockquote speaker notes, GFM tables. Legacy `.ppt` flattens tables to + bare text lines. + - EPUB: `#` chapter headings and internal anchor links. +3. **Write large outputs to a file with `-o`.** `-o out.md` keeps stdout silent + and gives a reviewable file instead of streaming the whole document into + context. +4. **Verify tables survived.** If the source had tables and the output has no + `|` rows, consult the format caveats — PDF and legacy `.ppt` flatten tables + by design, not by error. + +**Stop when** the conversion exits 0 and the structural markers match the +source format. Do not re-run or retry on a documented failure mode (encrypted, +malformed, scanned/image-only, unsupported) without changing the input; report +the documented message and route as [references/errors.md](references/errors.md) +instructs. diff --git a/anydoc/evals/evals.json b/anydoc/evals/evals.json new file mode 100644 index 0000000..40de679 --- /dev/null +++ b/anydoc/evals/evals.json @@ -0,0 +1,159 @@ +{ + "schema_version": 1, + "skill_name": "anydoc", + "evals": [ + { + "id": "docx-headings", + "prompt": "Convert this Word document to markdown and extract its headings.", + "expected_output": "The conversion exits 0 with empty stderr and emits GitHub-Flavored Markdown whose structure is expressed as ATX headings, including the lines `## Style heading stays a heading`, `### Direct level overrides the style`, and `# Direct outline without a style`.", + "assertions": [ + "The output contains the heading line `## Style heading stays a heading`", + "The output contains the heading line `### Direct level overrides the style`", + "The output contains the heading line `# Direct outline without a style`" + ], + "files": ["fixtures/fixture-handmade-outline.docx"] + }, + { + "id": "xlsx-table-cell-values", + "prompt": "Convert this spreadsheet to markdown and show me the cell values as a table.", + "expected_output": "The conversion exits 0 and emits a `## Values` heading followed by a GFM table whose cells carry the raw cell values: `Percent | 0.155`, `Currency | 1234.5`, `Thousands | 9876543`, `Date | 2026-03-15` — the number formats are dropped, so the output must NOT contain `15.5%` or `$1,234.50`.", + "assertions": [ + "The output contains a `## Values` heading", + "The output contains the table row `| Percent | 0.155 | fifteen and a half |`", + "The output contains the raw value `1234.5` for the Currency row", + "The output does not contain the formatted values `15.5%` or `$1,234.50`" + ], + "files": ["fixtures/sheet.xlsx"] + }, + { + "id": "pptx-slides-structure", + "prompt": "Convert this PowerPoint deck to markdown, keeping the slide structure.", + "expected_output": "The conversion exits 0 and preserves slide structure: slide titles like `Deck Title Slide` and `Numbers Slide` render as plain paragraphs (not headings), top-level bullets carry an indented nested detail, speaker notes render as blockquotes (`> Speaker note for the intro slide.`), and the slide table renders as a GFM table with the row `| North | 42 |`.", + "assertions": [ + "The output contains `Deck Title Slide` as a plain paragraph, not a markdown heading", + "The output contains the blockquote line `> Speaker note for the intro slide.`", + "The output contains the GFM table row `| North | 42 |`" + ], + "files": ["fixtures/pres.pptx"] + }, + { + "id": "csv-table", + "prompt": "Convert this CSV file to a markdown table.", + "expected_output": "The conversion exits 0 and renders the file as a single GFM table with the first row promoted to the header: `| Kind | Value | Note |` with a separator row and body rows carrying the cell values `15.5%` and `fifteen and a half`.", + "assertions": [ + "The output contains the header row `| Kind | Value | Note |`", + "The output contains a row carrying the cell values `15.5%` and `fifteen and a half`" + ], + "files": ["fixtures/fixture-sheet.csv"] + }, + { + "id": "legacy-doc-converts", + "prompt": "Convert this legacy .doc file to markdown.", + "expected_output": "The conversion exits 0 with empty stderr and emits the shared document serializer's GFM shape: a `# Fixture Document` title, `##` section headings (including `## Lists` and `## Table`), a GFM table with merged cells as empty covered spans, and `[^1]: ...` footnote definitions at the end of the document.", + "assertions": [ + "The output contains the heading `# Fixture Document`", + "The output contains at least three `##` section headings including `## Lists` and `## Table`", + "The output contains a `[^1]:` footnote definition block" + ], + "files": ["fixtures/text.doc"] + }, + { + "id": "image-only-pdf-no-ocr", + "prompt": "Convert this scanned PDF to markdown.", + "expected_output": "The conversion FAILS by design: exit code 1, empty stdout, and exactly one stderr line `anydoc: unsupported input: PDF has no extractable text (Scanned, 1 pages): OCR is required`. anydoc does not perform OCR; the correct response is to route the file to OCR tooling or the hosted Firecrawl Parse API, not to retry locally.", + "assertions": [ + "The conversion exits with code 1 and emits no markdown", + "Stderr contains the verbatim message `anydoc: unsupported input: PDF has no extractable text (Scanned, 1 pages): OCR is required`", + "The response states that OCR is required and routes to OCR tooling or Firecrawl Parse rather than retrying locally" + ], + "files": ["fixtures/scanned-image-only.pdf"] + }, + { + "id": "ods-preserved-values", + "prompt": "Convert this OpenDocument spreadsheet to markdown. I need the formatted display values.", + "expected_output": "The conversion exits 0 and emits a `## Values` heading plus a GFM table whose cells carry the FORMATTED display values — `Percent | 15.5%`, `Currency | $1,234.50`, `Thousands | 9,876,543` — explicitly contrasting with the xlsx/xls number-format drop.", + "assertions": [ + "The output contains the table row `| Percent | 15.5% | fifteen and a half |`", + "The output contains the formatted values `15.5%` and `$1,234.50`", + "The output does not contain the raw values `0.155` or `1234.5`" + ], + "files": ["fixtures/sheet.ods"] + }, + { + "id": "odt-converts", + "prompt": "Convert this ODT document to markdown and show me the structure.", + "expected_output": "The conversion exits 0 with empty stderr and emits the document shape shared with DOCX/DOC/RTF: a `# Fixture Document` title, `##` section headings, a GFM table, and `[^1]:` / `[^2]:` footnote definition lines at the end.", + "assertions": [ + "The output contains the heading `# Fixture Document`", + "The output contains `##` section headings such as `## Lists` and `## Table`", + "The output contains the footnote definition `[^1]: Footnote after an astral character.`" + ], + "files": ["fixtures/text.odt"] + }, + { + "id": "pdf-text-lower-fidelity", + "prompt": "Convert this text-based PDF to markdown. Will the table survive the conversion?", + "expected_output": "The conversion exits 0 with empty stderr and preserves top-level structure via `# Fixture Document` and `##` section headings (`## Lists`, `## Table`, `## Notes and special text`), but the PDF pipeline is lower-fidelity: the table flattens into the plain paragraph `Wide head End Tall B2 C2 B3 C3` with no GFM table, footnote markers degrade to inline superscript glyphs with no `[^1]:` definition block, and links are not emitted as markdown links.", + "assertions": [ + "The output contains `# Fixture Document` and the `##` section headings including `## Table`", + "The table region flattens to the plain paragraph `Wide head End Tall B2 C2 B3 C3` with no GFM table row", + "The output contains no `[^1]:` footnote definition block" + ], + "files": ["fixtures/fixture-text.pdf"] + }, + { + "id": "legacy-ppt-flattens-tables", + "prompt": "Convert this legacy PowerPoint file to markdown and keep the slides' content.", + "expected_output": "The conversion exits 0 and preserves slide text: plain-paragraph titles `Deck Title Slide` and `Numbers Slide` and the blockquote speaker note `> Speaker note for the intro slide.`. The Numbers Slide table flattens to bare text lines (`Region`, `Total`, `North`, `42`) rather than a GFM table, unlike PPTX and ODP.", + "assertions": [ + "The output contains the blockquote line `> Speaker note for the intro slide.`", + "The table content renders as bare text lines including `North` and `42`", + "The output does not contain a GFM table row `| North | 42 |`" + ], + "files": ["fixtures/pres.ppt"] + }, + { + "id": "odp-slides-structure", + "prompt": "Convert this OpenDocument presentation to markdown, preserving the slide structure.", + "expected_output": "The conversion exits 0 and emits the same slide shape as PPTX: `Deck Title Slide` renders as a plain paragraph (not a heading), the speaker note renders as the blockquote `> Speaker note for the intro slide.`, and the slide table renders as a GFM table containing the row `| North | 42 |`.", + "assertions": [ + "The output contains `Deck Title Slide` as a plain paragraph, not a markdown heading", + "The output contains the blockquote line `> Speaker note for the intro slide.`", + "The output contains the GFM table row `| North | 42 |`" + ], + "files": ["fixtures/pres.odp"] + }, + { + "id": "rtf-converts", + "prompt": "Convert this RTF document to markdown and extract the structure.", + "expected_output": "The conversion exits 0 with empty stderr and emits the shared document serializer's shape: `# Fixture Document`, `##` section headings including `## Lists` and `## Table`, a GFM table with merged cells as empty covered spans, and `[^1]:` footnote definitions at the end.", + "assertions": [ + "The output contains the heading `# Fixture Document`", + "The output contains `##` section headings such as `## Lists` and `## Table`", + "The output contains the footnote definition `[^1]: Footnote after an astral character.`" + ], + "files": ["fixtures/text.rtf"] + }, + { + "id": "epub-converts", + "prompt": "Convert this EPUB ebook to markdown, keeping the chapter structure.", + "expected_output": "The conversion exits 0 with empty stderr and emits `# Fixture Book`, `# Chapter One` and `# Chapter Two` headings, a GFM table containing `| Bolts | 12 |`, and internal anchor links that resolve to fragments such as `[Chapter Two](#epub-text-ch002-xhtml-chapter-two)`.", + "assertions": [ + "The output contains the chapter heading `# Chapter One`", + "The output contains the GFM table row `| Bolts | 12 |`", + "The output contains the internal anchor link `[Chapter Two](#epub-text-ch002-xhtml-chapter-two)`" + ], + "files": ["fixtures/book.epub"] + }, + { + "id": "csv-quoted-cells", + "prompt": "Convert this CSV to a markdown table. Some cells contain commas and newlines.", + "expected_output": "The conversion exits 0 and renders the file as a single GFM table with the first row promoted to the header: `| name | desc | qty |` followed by a separator row, with quoted content intact — `| padded | comma, inside | 3 |` keeps the embedded comma and `| plain | multi line | 4 |` keeps the embedded newline.", + "assertions": [ + "The output contains the promoted header row `| name | desc | qty |`", + "The output contains the row `| padded | comma, inside | 3 |` with the embedded comma preserved" + ], + "files": ["fixtures/fixture-handmade-quoted.csv"] + } + ] +} diff --git a/anydoc/fixtures/book.epub b/anydoc/fixtures/book.epub new file mode 100644 index 0000000..4302964 Binary files /dev/null and b/anydoc/fixtures/book.epub differ diff --git a/anydoc/fixtures/empty--errors.docx b/anydoc/fixtures/empty--errors.docx new file mode 100644 index 0000000..e69de29 diff --git a/anydoc/fixtures/encrypted--errors.odt b/anydoc/fixtures/encrypted--errors.odt new file mode 100644 index 0000000..c336188 Binary files /dev/null and b/anydoc/fixtures/encrypted--errors.odt differ diff --git a/anydoc/fixtures/fixture-handmade-numbering.docx b/anydoc/fixtures/fixture-handmade-numbering.docx new file mode 100644 index 0000000..c437ff8 Binary files /dev/null and b/anydoc/fixtures/fixture-handmade-numbering.docx differ diff --git a/anydoc/fixtures/fixture-handmade-outline.docx b/anydoc/fixtures/fixture-handmade-outline.docx new file mode 100644 index 0000000..e3eee4a Binary files /dev/null and b/anydoc/fixtures/fixture-handmade-outline.docx differ diff --git a/anydoc/fixtures/fixture-handmade-quoted.csv b/anydoc/fixtures/fixture-handmade-quoted.csv new file mode 100644 index 0000000..ebc29c5 --- /dev/null +++ b/anydoc/fixtures/fixture-handmade-quoted.csv @@ -0,0 +1,4 @@ +name,desc,qty +" padded ","comma, inside",3 +plain,"multi +line",4 diff --git a/anydoc/fixtures/fixture-handmade-rich.docx b/anydoc/fixtures/fixture-handmade-rich.docx new file mode 100644 index 0000000..1002d82 Binary files /dev/null and b/anydoc/fixtures/fixture-handmade-rich.docx differ diff --git a/anydoc/fixtures/fixture-handmade-semicolon.csv b/anydoc/fixtures/fixture-handmade-semicolon.csv new file mode 100644 index 0000000..cd407d6 --- /dev/null +++ b/anydoc/fixtures/fixture-handmade-semicolon.csv @@ -0,0 +1,3 @@ +a;b;c +"1,5";"2,5";x +"3,0";y;z diff --git a/anydoc/fixtures/fixture-handmade-tables.docx b/anydoc/fixtures/fixture-handmade-tables.docx new file mode 100644 index 0000000..635c76b Binary files /dev/null and b/anydoc/fixtures/fixture-handmade-tables.docx differ diff --git a/anydoc/fixtures/fixture-handmade-utf16.csv b/anydoc/fixtures/fixture-handmade-utf16.csv new file mode 100644 index 0000000..77191f1 Binary files /dev/null and b/anydoc/fixtures/fixture-handmade-utf16.csv differ diff --git a/anydoc/fixtures/fixture-sheet.csv b/anydoc/fixtures/fixture-sheet.csv new file mode 100644 index 0000000..ab50c87 --- /dev/null +++ b/anydoc/fixtures/fixture-sheet.csv @@ -0,0 +1,8 @@ +Kind,Value,Note +Percent,15.5%,fifteen and a half +Currency,1234.5,dollars +Thousands,9876543,grouped +Date,2026-03-15,ides of March +Duration,26:30:15,over a day +Tiny,0.0000004,four ten-millionths +Boolean,TRUE,yes diff --git a/anydoc/fixtures/fixture-text.pdf b/anydoc/fixtures/fixture-text.pdf new file mode 100644 index 0000000..557e1d2 Binary files /dev/null and b/anydoc/fixtures/fixture-text.pdf differ diff --git a/anydoc/fixtures/handmade-merged.xlsx b/anydoc/fixtures/handmade-merged.xlsx new file mode 100644 index 0000000..8dd2442 Binary files /dev/null and b/anydoc/fixtures/handmade-merged.xlsx differ diff --git a/anydoc/fixtures/pres.odp b/anydoc/fixtures/pres.odp new file mode 100644 index 0000000..45d72f1 Binary files /dev/null and b/anydoc/fixtures/pres.odp differ diff --git a/anydoc/fixtures/pres.ppt b/anydoc/fixtures/pres.ppt new file mode 100644 index 0000000..1e0b433 Binary files /dev/null and b/anydoc/fixtures/pres.ppt differ diff --git a/anydoc/fixtures/pres.pptx b/anydoc/fixtures/pres.pptx new file mode 100644 index 0000000..de93ada Binary files /dev/null and b/anydoc/fixtures/pres.pptx differ diff --git a/anydoc/fixtures/scanned-image-only.pdf b/anydoc/fixtures/scanned-image-only.pdf new file mode 100644 index 0000000..b231f15 Binary files /dev/null and b/anydoc/fixtures/scanned-image-only.pdf differ diff --git a/anydoc/fixtures/sheet.ods b/anydoc/fixtures/sheet.ods new file mode 100644 index 0000000..5c1558c Binary files /dev/null and b/anydoc/fixtures/sheet.ods differ diff --git a/anydoc/fixtures/sheet.xls b/anydoc/fixtures/sheet.xls new file mode 100644 index 0000000..6d830f1 Binary files /dev/null and b/anydoc/fixtures/sheet.xls differ diff --git a/anydoc/fixtures/sheet.xlsx b/anydoc/fixtures/sheet.xlsx new file mode 100644 index 0000000..44dba2b Binary files /dev/null and b/anydoc/fixtures/sheet.xlsx differ diff --git a/anydoc/fixtures/text.doc b/anydoc/fixtures/text.doc new file mode 100644 index 0000000..7de1e81 Binary files /dev/null and b/anydoc/fixtures/text.doc differ diff --git a/anydoc/fixtures/text.odt b/anydoc/fixtures/text.odt new file mode 100644 index 0000000..a9fdba5 Binary files /dev/null and b/anydoc/fixtures/text.odt differ diff --git a/anydoc/fixtures/text.rtf b/anydoc/fixtures/text.rtf new file mode 100644 index 0000000..24fc6ca --- /dev/null +++ b/anydoc/fixtures/text.rtf @@ -0,0 +1,177 @@ +{\rtf1\ansi\deff3\adeflang1025 +{\fonttbl{\f0\froman\fprq2\fcharset0 Times New Roman;}{\f1\froman\fprq2\fcharset2 Symbol;}{\f2\fswiss\fprq2\fcharset0 Arial;}{\f3\froman\fprq2\fcharset0 Liberation Serif{\*\falt Times New Roman};}{\f4\fswiss\fprq2\fcharset0 Liberation Sans{\*\falt Arial};}{\f5\fnil\fprq2\fcharset0 Microsoft YaHei;}{\f6\fnil\fprq2\fcharset0 Arial;}{\f7\fswiss\fprq0\fcharset128 Arial;}} +{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;} +{\stylesheet{\s0\snext0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052 Normal;} +{\s1\snext25\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl0\outlinelevel0\nowidctlpar\hyphpar0\ltrpar\fs36\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052 heading 1;} +{\s2\snext25\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl1\outlinelevel1\nowidctlpar\hyphpar0\ltrpar\fs30\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052 heading 2;} +{\s3\snext25\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl2\outlinelevel2\nowidctlpar\hyphpar0\ltrpar\fs26\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052 heading 3;} +{\*\cs15\snext15\loch\b0 NotBold;} +{\*\cs16\snext16\loch\b Bold;} +{\*\cs17\snext17\loch\i Italic;} +{\*\cs18\snext18\loch\strike Struck;} +{\*\cs19\snext19\loch\cf9\ul\ulc0 Hyperlink;} +{\*\cs20\snext20 Footnote Characters;} +{\*\cs21\snext21\loch\super footnote reference;} +{\*\cs22\snext22 Endnote Characters;} +{\*\cs23\snext23\loch\super endnote reference;} +{\s24\sbasedon0\snext25\rtlch\af6\afs28 \ltrch\hich\af4\loch\sb240\sa120\keepn\f4\fs28\dbch\af5 Heading;} +{\s25\sbasedon0\snext25\loch\sl276\slmult1\sb0\sa140 Body Text;} +{\s26\sbasedon25\snext26\rtlch\af7 \ltrch List;} +{\s27\sbasedon0\snext27\rtlch\af7\afs24\ai \ltrch\loch\sb120\sa120\noline\fs24\i caption;} +{\s28\sbasedon0\snext28\rtlch\af7 \ltrch\loch\noline Index;} +{\s29\snext29\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\b\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052 StrongPara;} +{\s30\sbasedon0\snext30\loch\nowidctlpar\noline Table Contents;} +{\s31\sbasedon0\snext31\rtlch\afs20 \ltrch\loch\fi-340\li340\lin340\noline\fs20 footnote text;} +{\s32\sbasedon0\snext32\rtlch\afs20 \ltrch\loch\fi-340\li340\lin340\noline\fs20 endnote text;} +{\s33\sbasedon0\snext33 Frame Contents;} +}{\*\listtable{\list\listtemplateid1 +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0} +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0} +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0} +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0} +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0} +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0} +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0} +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0} +{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}\listid1} +{\list\listtemplateid2 +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow2{\leveltext \'02\'00.;}{\levelnumbers\'01;}\fi0\li0} +{\listlevel\levelnfc4\leveljc0\levelstartat1\levelfollow2{\leveltext \'02\'01);}{\levelnumbers\'01;}\fi0\li0} +{\listlevel\levelnfc2\leveljc0\levelstartat1\levelfollow2{\leveltext \'02\'02.;}{\levelnumbers\'01;}\fi0\li0} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'03.;}{\levelnumbers\'01;}\fi-360\li1800} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'04.;}{\levelnumbers\'01;}\fi-360\li2160} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'05.;}{\levelnumbers\'01;}\fi-360\li2520} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'06.;}{\levelnumbers\'01;}\fi-360\li2880} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'07.;}{\levelnumbers\'01;}\fi-360\li3240} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'08.;}{\levelnumbers\'01;}\fi-360\li3600}\listid2} +{\list\listtemplateid3 +{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow2{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f9\fi0\li0} +{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow2{\leveltext \'01\u9702 ?;}{\levelnumbers;}\f9\fi0\li0} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'02.;}{\levelnumbers\'01;}\fi-360\li1440} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'03.;}{\levelnumbers\'01;}\fi-360\li1800} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'04.;}{\levelnumbers\'01;}\fi-360\li2160} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'05.;}{\levelnumbers\'01;}\fi-360\li2520} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'06.;}{\levelnumbers\'01;}\fi-360\li2880} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'07.;}{\levelnumbers\'01;}\fi-360\li3240} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'08.;}{\levelnumbers\'01;}\fi-360\li3600}\listid3} +{\list\listtemplateid4 +{\listlevel\levelnfc1\leveljc0\levelstartat1\levelfollow2{\leveltext \'02\'00.;}{\levelnumbers\'01;}\fi0\li0} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'01.;}{\levelnumbers\'01;}\fi-360\li1080} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'02.;}{\levelnumbers\'01;}\fi-360\li1440} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'03.;}{\levelnumbers\'01;}\fi-360\li1800} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'04.;}{\levelnumbers\'01;}\fi-360\li2160} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'05.;}{\levelnumbers\'01;}\fi-360\li2520} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'06.;}{\levelnumbers\'01;}\fi-360\li2880} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'07.;}{\levelnumbers\'01;}\fi-360\li3240} +{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'08.;}{\levelnumbers\'01;}\fi-360\li3600}\listid4} +}{\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}{\listoverride\listid3\listoverridecount0\ls3}{\listoverride\listid4\listoverridecount0\ls4}}{\*\generator LibreOffice/25.2.7.2$Windows_X86_64 LibreOffice_project/5cbfd1ab6520636bb5f7b99185aa69bd7456825d}{\info{\creatim\yr0\mo0\dy0\hr0\min0}{\revtim\yr0\mo0\dy0\hr0\min0}{\printim\yr0\mo0\dy0\hr0\min0}}{\*\userprops}\deftab709\deftab709\deftab709\deftab709\deftab709 +\hyphauto1\viewscale100\formshade\nobrkwrptbl\paperh16838\paperw11906\margl1134\margr1134\margt1134\margb1134\sectd\sbknone\sftnnar\saftnnrlc\sectunlocked1\pgwsxn11906\pghsxn16838\marglsxn1134\margrsxn1134\margtsxn1134\margbsxn1134\ftnbj\ftnstart1\ftnrstcont\ftnnar\fet2\aftnrstcont\aftnstart1\aftnnrlc +{\*\ftnsep\chftnsep}\pgndec\pard\plain \s1\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl0\outlinelevel0\nowidctlpar\hyphpar0\ltrpar\fs36\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \tab}\ls1 \fi0\li0\lin0\ql\fi0\li0\lin0\ltrpar{\loch +Fixture Document} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Plain paragraph with }{\loch\cs16\loch\b\loch +bold}{\loch +, }{\loch\cs17\loch\i\loch +italic}{\loch +, and }{\loch\cs18\loch\strike\loch +struck}{\loch + runs.} +\par \pard\plain \s29\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\b\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Style-bold paragraph with a }{\loch\cs15\loch\b0\loch +NotBold-styled span}{\loch + inside.} +\par \pard\plain \s2\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl1\outlinelevel1\nowidctlpar\hyphpar0\ltrpar\fs30\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \tab}\ls1 \fi0\li0\lin0\ql\fi0\li0\lin0\ltrpar{\loch +Lists} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain 1.\tab}\ilvl0\ls2 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +First numbered} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain 2.\tab}\ilvl0\ls2 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Second numbered} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain a)\tab}\ilvl1\ls2 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Alpha sub one} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain b)\tab}\ilvl1\ls2 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Alpha sub two} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain i.\tab}\ilvl2\ls2 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Roman sub sub} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain 3.\tab}\ilvl0\ls2 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Third numbered} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Interrupting paragraph between lists.} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain 4.\tab}\ilvl0\ls2 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Fourth, continuing the count} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain IV.\tab}\ilvl0\ls4 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Roman starting at four} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain V.\tab}\ilvl0\ls4 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Roman five} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls3 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Bullet one} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls3 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Bullet two} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \u9702\'3f\tab}\ilvl1\ls3 \fi0\li0\lin0\ql\tx0\fi0\li0\lin0\ltrpar{\loch +Nested bullet} +\par \pard\plain \s2\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl1\outlinelevel1\nowidctlpar\hyphpar0\ltrpar\fs30\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \tab}\ls1 \fi0\li0\lin0\ql\fi0\li0\lin0\ltrpar{\loch +Table} +\par \trowd\trql\ltrrow\trpaddft3\trpaddt0\trpaddfl3\trpaddl0\trpaddfb3\trpaddb0\trpaddfr3\trpaddr0\cellx6425\cellx9638\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\intbl\ql\ltrpar{\loch +Wide head}\cell\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\intbl\ql\ltrpar{\loch +End}\cell\row\pard \trowd\trql\ltrrow\trpaddft3\trpaddt0\trpaddfl3\trpaddl0\trpaddfb3\trpaddb0\trpaddfr3\trpaddr0\clvmgf\cellx3213\cellx6425\cellx9638\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\intbl\ql\ltrpar{\loch +Tall}\cell\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\intbl\ql\ltrpar{\loch +B2}\cell\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\intbl\ql\ltrpar{\loch +C2}\cell\row\pard \trowd\trql\ltrrow\trpaddft3\trpaddt0\trpaddfl3\trpaddl0\trpaddfb3\trpaddb0\trpaddfr3\trpaddr0\clvmrg\cellx3213\cellx6425\cellx9638\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\intbl\ql\ltrpar\loch +\cell\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\intbl\ql\ltrpar{\loch +B3}\cell\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\intbl\ql\ltrpar{\loch +C3}\cell\row\pard \pard\plain \s2\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl1\outlinelevel1\nowidctlpar\hyphpar0\ltrpar\fs30\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \tab}\ls1 \fi0\li0\lin0\ql\fi0\li0\lin0\ltrpar{\loch +Notes and special text} +\par \ql\fi-340\li340\lin340\noline\ltrpar\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Music clef \uc0 \u55348\uc1 \u56606\'3f appears before this footnote}{{\super \loch \chftn{\*\footnote \chftn\pard\plain \s31\rtlch\afs20 \ltrch\loch\fi-340\li340\lin340\noline\fs20{\loch +\tab Footnote after an astral character.} +}} +}{\loch + reference.} +\par \ql\fi-340\li340\lin340\noline\ltrpar\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +An endnote follows here}{{\super \loch \chftn{\*\footnote\ftnalt \chftn\pard\plain \s32\rtlch\afs20 \ltrch\loch\fi-340\li340\lin340\noline\fs20{\loch +\tab Endnote body text.} +}} +}{\loch +.} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Persian with ZWNJ: }{ +\u1605\'3f\u1740\'3f\u8204\'3f\u1582\'3f\u1608\'3f\u1575\'3f\u1607\'3f\u1605\'3f}{\loch +. Family emoji: \uc0 \u55357\uc1 \u56424\'3f\u8205\'3f\uc0 \u55357\uc1 \u56425\'3f\u8205\'3f\uc0 \u55357\uc1 \u56423\'3f.} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Markdown specials: *stars* _under_ [bracket] `tick` #hash 1. dotted | pipe.} +\par \pard\plain \s2\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl1\outlinelevel1\nowidctlpar\hyphpar0\ltrpar\fs30\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \tab}\ls1 \fi0\li0\lin0\ql\fi0\li0\lin0\ltrpar{\loch +Links and anchors} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +External link to }{{\field{\*\fldinst HYPERLINK "https://example.com/page" }{\fldrslt {\loch\loch\cf9\ul\ulc0\loch +example}{}}}\loch +.} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Relative link to }{{\field{\*\fldinst HYPERLINK "file:///anydoc/tests/fixture-src/sibling.odt" }{\fldrslt {\loch\loch\cf9\ul\ulc0\loch +a sibling file}{}}}\loch +.} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +{\*\bkmkstart plainmark}{\*\bkmkend plainmark}This plain paragraph carries a bookmark.} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Jump to }{{\field{\*\fldinst HYPERLINK "#plainmark" }{\fldrslt {\loch\loch\cf9\ul\ulc0\loch +the bookmarked paragraph}{}}}\loch +.} +\par \pard\plain \s2\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl1\outlinelevel1\nowidctlpar\hyphpar0\ltrpar\fs30\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \tab}\ls1 \fi0\li0\lin0\ql\fi0\li0\lin0\ltrpar{\loch +Objects} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Inline image: }{\loch +{\pict{\*\picprop{\sp{\sn wzDescription}{\sv tiny red dot}}{\sp{\sn wzName}{\sv }}}\picscalex1700\picscaley1700\piccropl0\piccropr0\piccropt0\piccropb0\picw1\pich1\picwgoal10\pichgoal10\pngblip +89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d4944415478da63fccfc0500f000485018084a98c21000000004945 +4e44ae426082} +}{\loch + done.} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Text box: }{ +{\shp{\*\shpinst\shpwr2\shpwrk0\shpbypara\shpbyignore\shptop-1362\shpbottom-228\shpbxcolumn\shpbxignore\shpleft0\shpright2835\shpz1{\sp{\sn shapeType}{\sv 202}}{\sp{\sn dxWrapDistLeft}{\sv 72390}}{\sp{\sn dxWrapDistRight}{\sv 72390}}{\sp{\sn dyWrapDistTop}{\sv 72390}}{\sp{\sn dyWrapDistBottom}{\sv 72390}}{\sp{\sn posrelv}{\sv 2}}{\sp{\sn posv}{\sv 1}}{\sp{\sn posrelh}{\sv 2}}{\sp{\sn posh}{\sv 2}}{\sp{\sn dxTextLeft}{\sv 53975}}{\sp{\sn dyTextTop}{\sv 53975}}{\sp{\sn dxTextRight}{\sv 53975}}{\sp{\sn dyTextBottom}{\sv 53975}}{\sp{\sn lineColor}{\sv 0}}{\sp{\sn lineWidth}{\sv 635}}{\shptxt\s33\ql{\loch +Inside the text box.} +\par \pard}}} +}{\loch + after the box.} +\par \pard\plain \s2\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\ilvl1\outlinelevel1\nowidctlpar\hyphpar0\ltrpar\fs30\b\cf0\f3\lang2057\kerning1\dbch\af8\langfe2052{\listtext\pard\plain \tab}\ls1 \fi0\li0\lin0\ql\fi0\li0\lin0\ltrpar{\loch +Quote and code} +\par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang2057\langfe2052\hich\af3\loch\nowidctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang2057\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +Value below one millionth: 0.0000004 should survive.} +\par } \ No newline at end of file diff --git a/anydoc/fixtures/unsupported.xyz b/anydoc/fixtures/unsupported.xyz new file mode 100644 index 0000000..3b18e51 --- /dev/null +++ b/anydoc/fixtures/unsupported.xyz @@ -0,0 +1 @@ +hello world diff --git a/anydoc/references/cli-reference.md b/anydoc/references/cli-reference.md new file mode 100644 index 0000000..7a76560 --- /dev/null +++ b/anydoc/references/cli-reference.md @@ -0,0 +1,194 @@ +# CLI reference: the Any Doc CLI (pinned @firecrawl/anydoc@0.1.6) + +Everything here was captured by running the pinned CLI on this machine +(`npx -y @firecrawl/anydoc@0.1.6`, version 0.1.6, Node v22). The CLI is a 4.7 KB +Node wrapper (`bin.anydoc = cli.js`) around a native NAPI binding that ships as +an npm `optionalDependency` per platform. + +## Verbatim `--help` output + +``` +anydoc: convert documents to GitHub-Flavored Markdown + +Usage: + anydoc [options] + anydoc - [options] < file + +Converts one document per invocation and writes the Markdown to stdout. +Pass - as the input to read the document from stdin. Never prompts; all +diagnostics go to stderr. + +Options: + -o, --output Write the Markdown to instead of stdout + -f, --format Name the input format instead of detecting it: + doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv + (extension aliases like xls, docm, ppsx resolve + to these) + -h, --help Print this help and exit + -V, --version Print the version and exit + +The format is detected from the file content; the file extension is the +fallback for signature-less formats (CSV). stdin has no extension, so CSV +input from stdin needs --format csv. Scanned or image-only PDFs need OCR, +which anydoc does not do, and error as unsupported. + +Exit codes: + 0 success + 1 the document could not be read or converted + 2 usage error: unknown option, missing input, or invalid --format + +Examples: + anydoc report.docx + anydoc slides.pptx -o slides.md + anydoc - --format csv < data.csv + curl -s https://example.com/paper.pdf | anydoc - +``` + +`anydoc --version` prints exactly `0.1.6` (verified; both `--help` and +`--version` exit 0 and write to stdout). + +## Invocation forms + +```text +anydoc [options] # convert a path on disk +anydoc - [options] < file # read the document from stdin +``` + +- `-` as the input reads the document from **stdin**. +- The CLI accepts **exactly one document per invocation** — there is no batch + mode. Passing a second input exits 2: + `anydoc: one document per invocation: unexpected second input ''`. + For multiple documents use a shell loop or `scripts/anydoc batch` + (see [workflows.md](workflows.md)). + +## Flag reference + +| Token | Behavior (verified) | +| --- | --- | +| `` | Input path. Format detected from content; extension is the fallback for signature-less formats (CSV). | +| `-` | Read the document from stdin. If stdin is a TTY, exits 2 with `anydoc: stdin is a terminal; pipe or redirect a document into anydoc -`. | +| `-o `, `--output ` | Write the Markdown to `` instead of stdout. **Silently overwrites** an existing file (verified). Writing to a directory fails with exit 1: `anydoc: EISDIR: illegal operation on a directory, open ''`. With `-o`, stdout stays silent. | +| `-f `, `--format ` | Force the input format instead of detecting it. Values: `doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv`. Extension aliases resolve through the parser mapping (verified: `--format xls`, `--format docm` accepted). Invalid value → exit 2: `anydoc: invalid format 'bogus'; expected one of: doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv`. | +| `-h`, `--help` | Print help to stdout, exit 0. Works even when the native binding is unavailable. | +| `-V`, `--version` | Print the version (`0.1.6`) to stdout, exit 0. Binding-independent like `--help`. | +| `--format=x` | Inline `=` value syntax is supported for long options (verified: `--format=rtf` works). | +| `--` | End of options: everything after `--` is treated as a positional input (a filename starting with `-`). | +| Missing option value | `anydoc: