Skip to content
Open
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
8 changes: 4 additions & 4 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ src/
diff/ # docx diff FILE --against SRC (markers.ts: locator normalization; renders read view, diffs via @core/diff)
comments/ # add | reply | resolve | delete | list (helpers.tsx: paraId, run-splitting)
images/ # list | extract | replace | delete
charts/ # list | extract (read-only; thin shell over @core/chart)
hyperlinks/ # add | list | replace | delete (wrap.tsx: run-splitting)
tables/ # row/column insert+delete | set-widths | merge | unmerge | borders (grid.ts: merge-aware model)
track-changes/ # on|off | list | accept | reject (apply.ts holds the unwrap/delete logic)
Expand All @@ -55,6 +56,7 @@ src/
ast/ # types + DocView + XML→AST reader; text.ts (paragraph helpers), sym.ts (<w:sym> decode)
locators/ # parse "p3:5-20" → Locator + resolve to refs
image/ # drawing.tsx (<Image> + addImagePart + collectImageRuns), source.ts (load path/data:/http + HEIC→JPEG), formats.ts (one mime↔ext table)
chart/ # native charts: read.ts (chart part → cached series), charts.ts (Charts lens), csv.ts
equation/ # latexToOmml / ommlToLatex bidirectional (temml + own MathML↔OMML adapter)
markdown/ # MarkdownImport lens: remark-parse + remark-gfm + remark-math + CriticMarkup plugin → walker → emitters
raw/ # Raw lens: fragment gate pipeline (element-order, namespaces+dcx:raw marker, reference audit) + libxml2-wasm XSD engine over bundled ECMA-376 schemas (schemas/)
Expand Down
16 changes: 15 additions & 1 deletion REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ docx endnotes list FILE
docx headers list FILE
docx footers list FILE
docx images list FILE
docx charts list FILE
docx hyperlinks list FILE
docx track-changes list FILE

Expand Down Expand Up @@ -133,6 +134,15 @@ size="…in" margins="…in" text-width="…in" -->` note when the page deviates
`float`/`wrap`/`align`/`overflow` only when they deviate (an inline, in-bounds
image shows just its size). `overflow` flags an image wider than the usable text
column (`ImageRun.floating`/`wrap`/`align` + EMU extents in `read --ast`).
- **Charts** (native Word charts) keep a `` `[chart]` `` token followed by a
`<!-- docx:chart chart0 type="column" title="Revenue" series="2" points="4" size="6x3.5in" -->`
note: `chartN` (the `charts extract --at` handle), `type` (a combo chart joins
its types: `column+line`), the series/points counts and `size` always; `title`
when there is one; `data="external"` / `data="none"` only when the chart isn't
backed by an embedded workbook; `float`/`wrap`/`align` as for images. The data
itself is `docx charts extract` (CSV) or `read --ast` (`ChartRun.series`, read
from the chart part's caches — what Word draws). Charts are read-only for now:
editing the paragraph around one keeps the chart part and its workbook intact.
- **Headers / footers** surface as `<!-- docx:header hdr0 text="Quarterly Report" -->`
/ `<!-- docx:footer ftr0 text="Page {page} of {pages}" -->` notes — led by the
marginal's `hdrN`/`ftrN` id, the handle `raw get --at` and `headers`/`footers
Expand Down Expand Up @@ -296,6 +306,9 @@ docx images extract FILE --to DIR [--at imgN] # --to = output directo
docx images replace FILE --at imgN --with ./new.png
docx images delete FILE --at imgN

docx charts list FILE # JSON array: {id, at, type, title?, series, points, data, workbook?}
docx charts extract FILE --at chartN [--json] # the chart's cached data as CSV (category,<series…>); --json = full series objects

docx hyperlinks add FILE --at pN:S-E --url URL
docx hyperlinks replace FILE --at linkN --with URL
docx hyperlinks delete FILE --at linkN
Expand Down Expand Up @@ -445,7 +458,7 @@ The CLI is built for non-interactive agents. **Exit code is the success signal**

## Discovering ids

Locators come in two flavors. **Positional block ids** (`pN`, `tN`, `sN`) are derived from document order and **shift after structural edits** — re-read between non-trivial mutations. **Entity ids** (`cN`, `imgN`, `linkN`, `fnN`, `enN`, `tcN`, `eqN`, `hdrN`, `ftrN`) are surfaced by a `list` verb (or `read --ast`) and are what you pass to `--at`:
Locators come in two flavors. **Positional block ids** (`pN`, `tN`, `sN`) are derived from document order and **shift after structural edits** — re-read between non-trivial mutations. **Entity ids** (`cN`, `imgN`, `chartN`, `linkN`, `fnN`, `enN`, `tcN`, `eqN`, `hdrN`, `ftrN`) are surfaced by a `list` verb (or `read --ast`) and are what you pass to `--at`:

| Id | Discover with | Used by |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
Expand All @@ -454,6 +467,7 @@ Locators come in two flavors. **Positional block ids** (`pN`, `tN`, `sN`) are de
| `fnN` / `enN` (foot/endnote) | `docx footnotes list FILE` / `docx endnotes list FILE` | `footnotes/endnotes edit/delete --at` |
| `hdrN` / `ftrN` (header/footer) | `docx headers list FILE` / `docx footers list FILE` (or `read --ast` / the `docx:header`/`docx:footer` note) | `raw get --at`, `headers/footers set/clear --at` (or scope by section: `--at sN --type T`) |
| `imgN` (image) | `docx images list FILE` | `images extract/replace/delete --at` |
| `chartN` (chart) | `docx charts list FILE` (or the `<!-- docx:chart chartN … -->` note / `read --ast`) | `charts extract --at` |
| `linkN` (hyperlink) | `docx hyperlinks list FILE` | `hyperlinks replace/delete --at` |
| `tcN` (tracked change) | `docx track-changes list FILE` | `track-changes accept/reject --at` |
| `eqN` (equation) | `docx read FILE --ast` (run `latex` field) | `equations edit --at eqN` |
Expand Down
4 changes: 2 additions & 2 deletions skills/docx-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ it if you use `--ast`.
text box N (a floating stamp / pull quote / letterhead box — `read` prints its
story right after the anchor paragraph between `docx:textbox tbxN` hints, and
`find` / `replace --all` include it).
- Entities: `cN` comment, `imgN` image, `linkN` hyperlink, `fnN`/`enN`
foot/endnote, `tcN` tracked change, `eqN` equation.
- Entities: `cN` comment, `imgN` image, `chartN` chart, `linkN` hyperlink,
`fnN`/`enN` foot/endnote, `tcN` tracked change, `eqN` equation.
- Get them from `docx read FILE` (locators ride the Markdown as `<!-- pN -->`
comments; a plain empty cell appears as `<!-- tN:rRcC -->`) or `docx read
FILE --ast` (lossless JSON).
Expand Down
1 change: 1 addition & 0 deletions skills/docx-cli/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ docx info skill # this skill, regenerated from the binary
| `footnotes` / `endnotes` | `add` (`--at` / `--anchor`), `edit`, `delete`, `list`. |
| `headers` / `footers` | `set` / `list` / `clear` — text, page numbers, dates, fields; e.g. `footers set FILE --page-number --of-pages`. |
| `images` | `add` (`--caption` for a figure), `extract`, `replace`, `delete`, `list`. |
| `charts` | `list` (`chartN`, type, title, series), `extract --at chartN` (data as CSV, or `--json`). Read-only for now. |
| `hyperlinks` | `add` (`--url`), `list`, `replace` (`--with`), `delete`. |
| `tables` | Insert/delete rows & columns, merge/unmerge, set widths, borders, formatting. |
| `lists` | Renumber a numbered list (`set --at pN --start 5` / `--format upper-roman` / `--restart` / `--continue`). |
Expand Down
9 changes: 9 additions & 0 deletions src/cli/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ Full contract in [src/core/markdown/CLAUDE.md](../core/markdown/CLAUDE.md). New
structural annotation? Use `formatNote`, keep it deviation-only, and remember the
importer won't reconstruct it — NO comment is parse-back, not even `docx:base`.

## `charts` — read-only chart surface

`docx charts list` / `charts extract --at chartN [--json]` are thin shells over
the `Charts` lens in [`@core/chart`](../core/chart/CLAUDE.md); `read` prints
each native chart as `` `[chart]` `` + `<!-- docx:chart chartN type=… title=…
series=N points=N size=… -->` (`formatChartNote` in `read/markdown.ts`, sharing
`chartTypeName`/`chartPointCount` with `list` so they can't disagree). An
unknown id fails `CHART_NOT_FOUND` (exit 3). No chart verb mutates yet.

## Help leads with markdown; `--text` is the literal escape hatch

The read view is Markdown, so the natural write-back is markdown — and it's the DEFAULT: `edit`/`insert` take the content as a POSITIONAL argument after the flags (`docx edit FILE --at p3 "Net **30** days"`), which `adoptPositionalMarkdown` (`cli/parse-helpers.ts`) folds into `values.markdown` before validation so every downstream check sees one path; `--markdown` stays as an alias. A second content positional is REFUSED (an unquoted phrase would otherwise silently truncate to its first word), as is positional + `--markdown` or positional + `--batch`. The `edit`/`insert` help (and the top-level one-liners) show the positional form FIRST, in the usage line and the examples, and describe `--text` as the LITERAL channel (`--text "**bold**"` writes the asterisks) for values Markdown would mangle or for a character span. Weak agents copy `**…**` from the read view straight into `--text`/`replace` and ship literal asterisks (the résumé b2-r3 run); three of five agents also read `edit --help` through `head -50`/`head -80`, so the contract sits in the first ten lines. Alignment/spacing/indent/tabs ride along with `--markdown` on both verbs (applied to every paragraph the source produces); only `--style` (and insert's `--list`) is refused next to it, because a `# heading` or list item owns those. `insert` also blends a new paragraph into its neighbor like Word's Enter key — font/size plus tabs, indent, alignment and spacing (`inheritParagraphLayout` in `core/insert/inherit.ts`) — unless the caller sets them; a heading anchor lends nothing, a list anchor lends no layout.
Expand Down
77 changes: 77 additions & 0 deletions src/cli/charts/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Charts, chartToCsv } from "@core/chart";
import {
EXIT,
fail,
openOrFail,
respond,
tryParseArgs,
writeStdout,
} from "../respond";

const HELP = `docx charts extract — print one chart's data

Usage:
docx charts extract FILE --at chartN [--json]

Required:
--at chartN The chart (e.g. chart0; see \`docx charts list\`)

Optional:
--json Print the full series objects (name, categories, values,
workbook refs, formatCode) instead of CSV
-h, --help Show this help

Output:
CSV (RFC 4180) by default: a header row "category,<series 1>,<series 2>,…",
then one row per category. An empty cell is a gap in the data, not 0.
The values are the chart's CACHE — what Word draws on the page. Redirect to
save it: docx charts extract report.docx --at chart0 > chart0.csv
Errors print {code, error, hint?} with a nonzero exit.

Examples:
docx charts extract report.docx --at chart0
docx charts extract report.docx --at chart1 --json | jq '.series[0].values'
`;

export async function run(args: string[]): Promise<number> {
const parsed = await tryParseArgs(
args,
{
at: { type: "string" },
json: { type: "boolean" },
help: { type: "boolean", short: "h" },
},
HELP,
);
if (typeof parsed === "number") return parsed;

if (parsed.values.help) {
await writeStdout(HELP);
return EXIT.OK;
}

const path = parsed.positionals[0];
if (!path) return fail("USAGE", "Missing FILE argument", HELP);

const chartId = parsed.values.at as string | undefined;
if (!chartId) return fail("USAGE", "Missing --at chartN", HELP);

const document = await openOrFail(path);
if (typeof document === "number") return document;

const chart = new Charts(document).get(chartId);
if (!chart) {
return fail(
"CHART_NOT_FOUND",
`Chart not found: ${chartId}`,
`Run "docx charts list ${path}" to see the chart ids.`,
);
}

if (parsed.values.json) {
await respond({ id: chart.id, title: chart.title, series: chart.series });
return EXIT.OK;
}
await writeStdout(chartToCsv(chart));
return EXIT.OK;
}
41 changes: 41 additions & 0 deletions src/cli/charts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { fail, writeStdout } from "../respond";

type CommandFn = (args: string[]) => Promise<number>;

const SUBCOMMANDS: Record<string, () => Promise<{ run: CommandFn }>> = {
extract: () => import("./extract"),
list: () => import("./list"),
};

const HELP = `docx charts — inspect native Word charts

Usage:
docx charts <verb> FILE [options]

Verbs:
list Print the chart manifest as JSON (id, type, title, series, points)
extract Print one chart's data as CSV (or --json) — the cached values Word draws

Charts show in \`docx read\` as \`[chart]\` plus a <!-- docx:chart chartN … --> hint;
chartN is the --at handle.

Run "docx charts <verb> --help" for verb-specific help.
`;

export async function run(args: string[]): Promise<number> {
const verb = args[0];
if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
await writeStdout(HELP);
return verb ? 0 : 2;
}
const loader = SUBCOMMANDS[verb];
if (!loader) {
return fail(
"USAGE",
`Unknown charts subcommand: ${verb}`,
'Run "docx charts --help".',
);
}
const module_ = await loader();
return module_.run(args.slice(1));
}
58 changes: 58 additions & 0 deletions src/cli/charts/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Charts } from "@core/chart";
import {
EXIT,
fail,
openOrFail,
respond,
tryParseArgs,
writeStdout,
} from "../respond";

const HELP = `docx charts list — print the chart manifest as JSON

Usage:
docx charts list FILE [options]

Options:
-h, --help Show this help

Output:
A bare JSON array, one object per native chart in document order:
{ id, at, type, title?, series, points, data, workbook? }.
id chartN — the --at handle for \`charts extract\`
at the paragraph holding the chart (pN, t0:r0c0:p0, tbx0:p0, …)
type chart type; a combo chart joins its types with "+" (column+line)
series series names
points number of categories plotted
data where Word's Edit Data reads from: embedded | external | none
workbook the embedded workbook's part name, when data is "embedded"
Errors print {code, error, hint?} with a nonzero exit.

Examples:
docx charts list report.docx | jq -c '.[] | {id, type, title}'
`;

export async function run(args: string[]): Promise<number> {
const parsed = await tryParseArgs(
args,
{
help: { type: "boolean", short: "h" },
},
HELP,
);
if (typeof parsed === "number") return parsed;

if (parsed.values.help) {
await writeStdout(HELP);
return EXIT.OK;
}

const path = parsed.positionals[0];
if (!path) return fail("USAGE", "Missing FILE argument", HELP);

const document = await openOrFail(path);
if (typeof document === "number") return document;

await respond(new Charts(document).list());
return EXIT.OK;
}
1 change: 1 addition & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Commands (each one-liner names capabilities you'd otherwise miss; see <command>
headers … Set/list/clear page headers — --text, page numbers, date, fields; --first-page/--even; default = whole document
footers … Set/list/clear page footers — e.g. "footers set FILE --page-number --of-pages" for "Page X of Y"
images … Add (--caption "Figure 1: …" for a captioned figure), extract, replace, delete, list images
charts … Inspect native Word charts — list (chartN, type, title, series), extract a chart's data as CSV (--at chartN); read shows each as \`[chart]\` + a docx:chart hint
hyperlinks … Add, list, replace, delete hyperlinks (add uses --url; replace uses --with)
tables … Create a table (create --after pN --rows N --cols M), then restructure — insert/delete rows & columns, merge/unmerge, set widths, borders, format
lists FILE Renumber a numbered list — "lists set --at pN --start 5" / "--format upper-roman" / "--restart" / "--continue"
Expand Down
1 change: 1 addition & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fail, writeStderr, writeStdout } from "./respond";
type CommandFn = (args: string[]) => Promise<number>;

const COMMANDS: Record<string, () => Promise<{ run: CommandFn }>> = {
charts: () => import("./charts"),
code: () => import("./code"),
comments: () => import("./comments"),
create: () => import("./create"),
Expand Down
2 changes: 2 additions & 0 deletions src/cli/info/locators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Discovering ids:
default Markdown render)
cN docx comments list FILE
imgN docx images list FILE
chartN docx charts list FILE
linkN docx hyperlinks list FILE
fnN / enN docx footnotes list FILE / docx endnotes list FILE
hdrN / ftrN docx headers list FILE / docx footers list FILE
Expand Down Expand Up @@ -186,6 +187,7 @@ function renderJson(): unknown {
"pN/tN/sN": "docx read FILE --ast",
cN: "docx comments list FILE",
imgN: "docx images list FILE",
chartN: "docx charts list FILE",
linkN: "docx hyperlinks list FILE",
"fnN/enN": "docx footnotes list FILE / docx endnotes list FILE",
"hdrN/ftrN": "docx headers list FILE / docx footers list FILE",
Expand Down
Loading
Loading