feat: comment extraction for DOCX and PPTX - #91
Merged
Conversation
Add comment extraction for DOCX, gated by the new `ConversionOptions::extract_comments` flag (default off). - New shared `converter::comments` module: the `Comment` model plus pure helpers (`collapse_ws`, `cap_text`, `format_author`) and the `# Comments` Markdown / plain-text renderers (`append_comments`). - DOCX parsing of `word/comments.xml` (author, date, body, paraId), `word/commentsExtended.xml` (reply detection), and commented-on text ranges (`commentRangeStart`/`End`, `commentReference`) across the body and any headers, footers, footnotes, and endnotes. - Comments are ordered by first anchor appearance in a fixed part sequence (body, headers, footers, footnotes, endnotes); unanchored (orphan) comments are appended last. Replies are flattened and marked `(reply)`. `author` renders as `Name (date)` (date verbatim, omitted when absent; `Unknown` when no author). `source` is whitespace-collapsed and capped at 200 characters. - `convert_inner` now returns the collected comments; the section is appended after image-placeholder resolution in both the sync and async DOCX paths. The section is appended to both `markdown` (formatted) and `plain_text` (markers stripped). Malformed or unresolved comment data is skipped with a warning, per the best-effort convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrew Radcliffe <andrewjradcliffe@gmail.com>
Extend comment extraction (gated by `ConversionOptions::extract_comments`) to PPTX, covering both comment schemes: - Legacy (PowerPoint 2007–2013): `ppt/commentAuthors.xml` author registry and `ppt/comments/commentN.xml` files (`p:cm` with plain-text `p:text` body, `authorId`/`dt`). - Modern (PowerPoint 2016+/365): `ppt/authors.xml` (GUID authors) and `ppt/comments/modernComment_*.xml` files (`p188:cm` with DrawingML `a:t` body, `authorId`/`created`); nested `p188:replyLst`/`p188:reply` replies are flattened, marked `(reply)`, and emitted after their parent in document order. Comment parts are discovered per slide via the slide relationships and dispatched by relationship type (the modern office/2018 namespace vs the legacy 2006 one). Since PPTX comments are point-anchored rather than text-anchored, `source` is the slide label (`Slide N: Title`, or `Slide N` when untitled). `convert_inner` returns the collected comments; the section is appended after image resolution in both the sync and async paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrew Radcliffe <andrewjradcliffe@gmail.com>
Expose comment extraction on the CLI via `--extract-comments`, mapped to `ConversionOptions::extract_comments` in `build_options`. The flag is a no-op for formats without comments. Add public-API end-to-end tests that build DOCX and PPTX files with comment parts in memory and assert the appended `# Comments` section via `convert_bytes`, plus CLI tests covering `--help` listing, the CSV no-op, and DOCX conversion under the flag. Also applies rustfmt normalization to the comment-extraction code added in the preceding commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrew Radcliffe <andrewjradcliffe@gmail.com>
Document the new `extract_comments` option and `--extract-comments` flag: - README: CLI usage example, a "Extracting Comments (DOCX / PPTX)" library section with example output, and a `ConversionOptions` table row. - TECH_SPEC: the new option field, plus comment rows and notes in the DOCX (§4.1) and PPTX (§4.2) extraction tables. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrew Radcliffe <andrewjradcliffe@gmail.com>
Address correctness and robustness issues found in code review of the comment-extraction feature: - Self-closing text elements (`<w:t/>`, `<p:text/>`, `<a:t/>`) no longer leave the text-capture flag stuck on and leak stray text into comment bodies: text capture is set only on Start events (Empty has no End). - DOCX commented-on source no longer leaks past its range: comment range markers are honored even inside skipped `mc:Choice` branches (so an end buried there still closes), text is captured only inside a run (matching the body parser), and each range's source is byte-bounded so a malformed unclosed range cannot absorb the whole part. - DOCX `w14:paraId` is captured only from top-level paragraphs (not nested table cells), so reply detection is not thrown off by a trailing table. - PPTX comment `source` (slide label) is whitespace-collapsed and capped, so a multi-line slide title no longer breaks the single-line list item. - A slide carrying both legacy and modern comment parts no longer double-reports comments (modern is preferred). - Author/date attributes are XML-unescaped (e.g. "R&D" → "R&D"), matching body-text handling, via a shared `attr_value_unescaped` helper. - `collapse_ws` collapses only ASCII whitespace, preserving meaningful non-ASCII spaces (NBSP, ideographic space) per the Unicode-fidelity goal. - Comment parts are read with a lossy UTF-8 decode so a single malformed sub-part degrades gracefully instead of aborting the whole conversion. - The two PPTX author-registry parsers are merged into one parameterized `parse_author_registry`; the `cap_text` doc is corrected to note scalar-level (not grapheme-level) truncation; orphan-id sort uses a total-order key. Each fix has a dedicated regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrew Radcliffe <andrewjradcliffe@gmail.com>
Merged
andrewjradcliffe
pushed a commit
to andrewjradcliffe/anytomd-rs
that referenced
this pull request
Jun 1, 2026
Minor bump (1.2.2 -> 1.3.0): developer0hye#91 added opt-in comment extraction for DOCX and PPTX — a new public `ConversionOptions.extract_comments` field and a `--extract-comments` CLI flag — which is new, non-breaking public API per SemVer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds opt-in extraction of document comments for DOCX and PPTX. When enabled,
a
# Commentssection is appended to the end of the converted output, capturingthe commenter, the comment body, and the source the comment is attached to.
Exposed two ways:
ConversionOptions.extract_comments: bool(defaultfalse).--extract-commentsflag.The flag is a no-op for formats without comments, so it is always safe to pass.
Motivation
Comments carry review context — questions, decisions, action items — that is
otherwise lost on conversion. For LLM consumption (this crate's target), that
context is often as valuable as the body text. The feature is opt-in so default
output stays clean and existing behavior is unchanged.
Output format
The appended section uses a fixed, flat structure (global 1‑based index in order
of appearance):
The section is appended to both
markdown(formatted, above) andplain_text(the same layout with all Markdown markers stripped:Comments/1/author:/comment:/source:), consistent with thecrate's dual-output principle. With zero comments, the section is omitted
entirely.
Behavior
Common
authorrenders asName (date); the date is emitted verbatim (no parsing)and omitted when absent (no empty parentheses). A missing author becomes
Unknown.meaningful non-ASCII spaces such as NBSP/ideographic space are preserved).
(reply).ConversionWarning(best-effort), so it surfaces on stderr and trips
--strict.DOCX
word/comments.xmlfor author/date/body andword/commentsExtended.xmlfor reply threading.
sourceis the text inside thecommentRangeStart…commentRangeEndspan, scanned across the body and any headers, footers,footnotes, and endnotes. Ordered by first anchor appearance in a fixed part
sequence (body → headers → footers → footnotes → endnotes); comments with no
anchor are appended last. Source is collapsed to one line and capped at 200
characters.
PPTX
commentAuthors.xml+comments/comment*.xml) andmodern (
authors.xml+comments/modernComment_*.xml, threaded) schemes.sourceis theslide label (
Slide N: Title, orSlide Nwhen the slide is untitled).Key changes
src/converter/comments.rs(new): theCommentmodel, normalization helpers(
collapse_ws,cap_text,format_author), and the# CommentsMarkdown /plain-text renderers (
append_comments).src/converter/docx.rs: comment parsing (comments.xml,commentsExtended.xml, range collection across content parts) and assembly,wired into
convert_inner.src/converter/pptx.rs: legacy + modern comment/author parsers and per-slidecollection, wired into
convert_inner.src/converter/ooxml_utils.rs: a sharedattr_value_unescapedhelper thatXML-unescapes attribute values by local name.
src/zip_utils.rs:read_zip_text_lossyfor best-effort reads of optionalsub-parts.
src/converter/mod.rs,src/lib.rs,src/runner.rs: theextract_commentsoption, the async append sites, and the CLI flag.README.md,TECH_SPEC.md: documentation of the option, flag, output format,and per-format caveats.
convert_innernow returns the collected comments alongside the existing resultand pending-image data; the section is appended after image-placeholder
resolution in both the sync and async paths, so it never interferes with image
description.
Robustness (review hardening)
A follow-up commit addresses correctness/robustness issues found in review, each
with a regression test:
<w:t/>,<p:text/>,<a:t/>) no longer leakstray text into comment bodies.
sourceno longer leaks past its range: range markers are honored eveninside skipped
mc:Choicebranches, text is captured only inside a run, andeach range's source is byte-bounded so a malformed unclosed range cannot
absorb the rest of the part.
w14:paraIdis taken only from top-level paragraphs, so a comment endingin a table doesn't break reply detection.
double-reports (modern is preferred).
sourceitem.R&D→R&D), matching bodyhandling.
aborting the whole conversion.
Testing
author/reply parsing, edge cases) plus end-to-end tests through the public
convert_bytesAPI for DOCX and PPTX, and CLI tests for the flag.cargo test(all unit + integration suites),cargo clippy -- -D warnings,cargo fmt --check,cargo doc --no-deps(zero warnings),the
asyncandasync-geminifeature checks, the WASM target checks, andcargo build --release— all green.Notes / limitations
specification and validated against synthetic fixtures; comment parsing is
best-effort.
sourcetruncation is on a Unicode scalar boundary (panic-safe); multi-scalargrapheme clusters (e.g. flag/ZWJ emoji) may be split in the truncated tail.