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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- A `.pptx` can be edited and saved: every text operation a `.docx` takes, and
a save that writes the slide parts back into the package. Its
`FileTypeCapabilities` now states `edit` and `save`.

- A text document is edited the way a reader expects: typing, replacing and
deleting across runs and paragraphs, Enter, Backspace at a paragraph start,
and a plain-text paste that opens a paragraph per line.
Expand Down
9 changes: 4 additions & 5 deletions docs/design/document-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ mode frame in [`editing.md`](editing.md), and the decisions that are its own.
the sheet view. Text documents, presentations and drawings share this one:
what it edits is runs and paragraphs, wherever the format puts them.

Status: **the schema and the replay are landing; the browser editor follows.**
This document is written ahead of the code, and each section says what is in
and what is not.
Status: **landed.** The schema, the replay, the browser editor and the pptx
write side are all in the code; each section says what is in and what is not.

Scope of this work: an edit that spans several runs, a new paragraph, and a
delete or a replace that reaches across both. Inline formatting (bold, italic,
Expand Down Expand Up @@ -274,7 +273,7 @@ reading forward.
|---|---|---|
| `.odt`, `.odp`, `.ods`, `.odg` | `odf` | edits and saves today; the new ops land here |
| `.docx` | `ooxml/text` | edits and saves today; the new ops land here |
| `.pptx` | `ooxml/presentation` | **read-only today.** It already has `text_set_content` and keeps its slide DOMs resident; what it lacks is `save`, the two flags and the capability row |
| `.pptx` | `ooxml/presentation` | edits and saves; the same operations over `a:p` / `a:r` |
| everything else | β€” | read-only, and says so by decision 7 |

`.odp` needs nothing of its own: a presentation is the same odf `Document` as a
Expand All @@ -295,7 +294,7 @@ Each step is a pull request that builds and tests on its own.
4. **The browser editor.** Owns the DOM mutation, records the ops, and carries
undo/redo (decisions 6 and 6b). **Landed.**
5. **pptx writes.** `save`, `is_editable`, `is_savable`, the capability row and
the new hooks over `a:p` / `a:r`.
the new hooks over `a:p` / `a:r`. **Landed.**

## What a split does to what is around it

Expand Down
4 changes: 3 additions & 1 deletion src/odr/internal/file_type_table.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,9 @@ constexpr std::array table{
.open = true,
.decrypt = true,
.translate_html = true,
.color_scheme = true}},
.color_scheme = true,
.edit = true,
.save = true}},
Row{FileType::office_open_xml_workbook,
"xlsx"sv,
xlsx_extensions,
Expand Down
4 changes: 2 additions & 2 deletions src/odr/internal/ooxml/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ detection**; each is a self-contained module with its own `AGENTS.md`:
| Module | Format | Editable | Agent doc |
|---|---|:--:|---|
| [`text/`](text/) | `.docx` (Word) | text + save | [text/AGENTS.md](text/AGENTS.md) |
| [`presentation/`](presentation/) | `.pptx` (PowerPoint) | read-only | [presentation/AGENTS.md](presentation/AGENTS.md) |
| [`spreadsheet/`](spreadsheet/) | `.xlsx` (Excel) | read-only | [spreadsheet/AGENTS.md](spreadsheet/AGENTS.md) |
| [`presentation/`](presentation/) | `.pptx` (PowerPoint) | text + save | [presentation/AGENTS.md](presentation/AGENTS.md) |
| [`spreadsheet/`](spreadsheet/) | `.xlsx` (Excel) | cell values + save | [spreadsheet/AGENTS.md](spreadsheet/AGENTS.md) |

## Shared element model (same as ODF)

Expand Down
14 changes: 9 additions & 5 deletions src/odr/internal/ooxml/ooxml_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <cstddef>
#include <cstring>
#include <stdexcept>
#include <string_view>

namespace odr::internal {

Expand Down Expand Up @@ -39,10 +40,13 @@ std::optional<FontStyle> font_style_from_value(const char *value) {

xml::NodeSpan ooxml::write_text_nodes(pugi::xml_node parent,
const pugi::xml_node before,
const std::string &text) {
const std::string &text,
const std::string_view prefix) {
xml::NodeSpan span;
const std::string text_tag = std::string(prefix) + ":t";
const std::string tab_tag = std::string(prefix) + ":tab";

const auto insert = [&](const char *name) {
const auto insert = [&](const std::string &name) {
const pugi::xml_node node = before
? parent.insert_child_before(name, before)
: parent.append_child(name);
Expand All @@ -56,7 +60,7 @@ xml::NodeSpan ooxml::write_text_nodes(pugi::xml_node parent,
// space at either end of a `w:t`, and a lone space is part of a `string`
// token - so the text says whether one is there, not the token type.
const auto insert_text = [&](const std::string &token) {
pugi::xml_node node = insert("w:t");
pugi::xml_node node = insert(text_tag);
if (token.starts_with(' ') || token.ends_with(' ')) {
node.append_attribute("xml:space").set_value("preserve");
}
Expand All @@ -75,14 +79,14 @@ xml::NodeSpan ooxml::write_text_nodes(pugi::xml_node parent,
break;
case xml::StringToken::Type::tabs:
for (std::size_t i = 0; i < token.string.size(); ++i) {
insert("w:tab");
insert(tab_tag);
}
break;
}
}

if (!span.first) {
insert("w:t");
insert(text_tag);
}
return span;
}
Expand Down
9 changes: 6 additions & 3 deletions src/odr/internal/ooxml/ooxml_util.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ class AbsPath;

namespace odr::internal::ooxml {

/// Writes @p text as `w:t` / `w:tab` nodes before @p before, or at the end of
/// @p parent where that is null. Empty text still gets a node to anchor to.
/// Writes @p text as `<@p prefix>:t` / `<@p prefix>:tab` nodes before
/// @p before, or at the end of @p parent where that is null. The prefix is the
/// whole difference between the two markup languages: `w:` and `a:`. Empty
/// text still gets a node to anchor to.
xml::NodeSpan write_text_nodes(pugi::xml_node parent, pugi::xml_node before,
const std::string &text);
const std::string &text,
std::string_view prefix);

std::optional<std::string> read_string_attribute(pugi::xml_attribute);
std::optional<Color> read_color_attribute(pugi::xml_attribute);
Expand Down
17 changes: 13 additions & 4 deletions src/odr/internal/ooxml/presentation/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The **why**; the feature checklist is in [`README.md`](README.md), the shared
OOXML mechanics (registry/adapter pattern, OPC relationships, encryption) in
[`../AGENTS.md`](../AGENTS.md). **Read-only.**
[`../AGENTS.md`](../AGENTS.md). **Reader + text editor + save.**

**Scope.** Read `ppt/presentation.xml` and each slide's shape tree into the
abstract model so the generic renderer lays out positioned frames. Paragraphs,
Expand Down Expand Up @@ -92,7 +92,16 @@ Coverage is in [`README.md`](README.md). Foundational gaps, roughly by value:
3. **Table cell styles unresolved.** Tables are wired (grid, spans, covered
cells, column widths/row heights), but `a:tcPr` (fills, borders, margins)
is not translated.
4. **Read-only.** `text_set_content` machinery exists but is dormant
(`element_is_editable` β†’ false); wiring edit + save (mirroring docx) is a
natural next step.
4. **Editing is text-content and the structure a text edit needs**, the same
surface `.docx` has: set a run's text, put a run beside one, remove an
element, and split, merge or insert a paragraph. The dom half is
`xml::TreeEditor`, shared with odf and ooxml text β€” only the tag names
differ, and those come from the nodes. No style editing, and no editing of
a shape, a picture or a table's furniture.

`save` re-serialises the slide parts and copies the rest of the package
through as bytes, so a part we never parsed survives untouched. The slides
are held by their `r:id`, which is how the slide-id list names them, so
`save` keeps the other direction β€” path to `r:id` β€” to know which part it
is writing.
5. **Listings, comments/annotations** not modelled.
4 changes: 2 additions & 2 deletions src/odr/internal/ooxml/presentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Roughly ordered by importance.
- [x] slide background (`p:bg`, inherited from layout / master)
- [ ] slide master / layout inheritance (beyond theme colors + background)
- [x] text extraction
- [ ] edit
- [ ] save
- [x] edit (text, and the structure a text edit needs)
- [x] save

### Content

Expand Down
Loading
Loading