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
47 changes: 31 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,34 @@ bytes ─▶ magic/open_strategy ─▶ DecodedFile ─▶ Document ─▶ Eleme

### The element-adapter pattern (every engine follows it)

- An **`ElementRegistry`**: a flat `std::vector<Element>` (id = index + 1); each
`Element` holds `parent`/`first_child`/`last_child`/`prev`/`next` ids and a
`type`, plus side maps for per-type payloads. Builders: `create_element` /
`create_*_element` / `append_child`. Minimal example:
`oldms/presentation/ppt_element_registry.*`.
**The machinery is shared — do not write it again.** `internal/common/element_registry.hpp`
and `internal/common/element_adapter.hpp` hold it; an engine writes only what
its own format has. Compact example: `rtf/rtf_element_registry.*` +
`rtf/rtf_document.cpp`.

- An **`ElementRegistry`**: `internal::ElementRegistry<Element, Id>` is the flat
store (id = index + 1, a `std::deque` so `create_element_` can hand back a
reference), with `element_at`, `append_child`, `link_child` and
`check_element_id`. The engine derives `struct RegistryElement final :
ElementNode<Id>` — adding a field only if it has one, as odf/ooxml add the
`pugi::xml_node` — and declares its per-type payloads as `SideTable<T>`
(hashed) or `SortedSideTable<T, Id>` (binary search, for payloads written in
id order). Both carry their own bounds check, so an accessor is
`return m_texts.at(id);` and nothing else. Only the `create_*_element` and
the secondary-chain `append_*` are per engine.
- An **`ElementIdentifier` is opaque, and 64 bits wide**: registry engines use it
as index + 1, `csv` packs a kind, a row and a column into its bits. An engine
wanting a narrower id narrows it inside its own store (`odf`'s
`ElementRegistry::StoredId`) and widens at the boundary.
- An **`ElementAdapter`**: one class implementing `abstract::ElementAdapter` (tree
navigation by id) and, via multiple inheritance, the per-type adapters it
supports (`SlideAdapter`, `ParagraphAdapter`, …). Each `*_adapter(id)` returns
`this` when the element is that type, else `nullptr`. Compact example:
`oldms/presentation/ppt_document.cpp`.
wanting a narrower id passes it as `internal::ElementRegistry`'s `Id`
(`odf::StoredId`) and widens at the boundary.
- An **`ElementAdapter`**: `internal::RegistryElementAdapter<Registry, Adapters…>`
implements the tree navigation over the registry, and its base
`internal::ElementAdapter<Adapters…>` inherits the per-type adapters named in
the pack and answers **every** `*_adapter(id)` hook from them — an engine
writes no hook of its own, it only lists the adapters. `element_is_unique` and
`element_is_self_locatable` default to true and `element_is_editable` to
false; override only where that is wrong (odf, ooxml text). An engine with no
registry (`csv`) derives from `internal::ElementAdapter` and writes its own
navigation.

`ElementType` is the shared enum in `src/odr/document_element.hpp`.

Expand All @@ -64,7 +78,7 @@ producer's layout recorded — odf's `text:soft-page-break` — are not parsed.
|------|------|
| `src/odr/*.hpp` | **Public API**: `file`, `document`, `document_element`, `html`, `style`, `quantity` (`Measure`), `odr`. |
| `src/odr/internal/abstract/` | Core interfaces: `File`/`DecodedFile`, `Document` + `ElementAdapter`, `Filesystem`, `Archive`, `HtmlService`. |
| `src/odr/internal/common/` | Reusable impls: `Path`/`AbsPath`, base `Document`, filesystem, `style`, table cursor/range, temp files. |
| `src/odr/internal/common/` | Reusable impls: `Path`/`AbsPath`, base `Document`, the shared `ElementRegistry` + `ElementAdapter`, filesystem, `style`, table cursor/range, temp files. |
| `src/odr/internal/util/` | Helpers: `byte_stream_util`, `string_util`, `stream_util`, `document_util`. |
| `src/odr/internal/magic.*`, `open_strategy.*` | File-type detection + open/dispatch. |
| `src/odr/internal/file_type_table.*` | **The** per-`FileType` table: extensions, MIME types, category, document type, `FileTypeCapabilities`. Every public lookup in `odr.hpp` is a thin forward into it — extend the table, not the lookups. |
Expand Down Expand Up @@ -247,9 +261,10 @@ Dispatch `release.yml` against main, publish the draft that appears —
alias is claimed twice, or if the declared capabilities exceed what the
engines actually do.
2. For documents: subclass `internal::Document`; in its constructor build an
`ElementRegistry` and an `ElementAdapter` (pattern above). It defaults to
read-only — override `is_editable`/`is_savable`/`save` only for an engine
that can write.
`ElementRegistry` (derived from `internal::ElementRegistry`) and an
`ElementAdapter` (derived from `internal::RegistryElementAdapter`) — pattern
above, and neither is written from scratch. It defaults to read-only —
override `is_editable`/`is_savable`/`save` only for an engine that can write.
3. Implement the per-element adapters you can populate; the **generic HTML
renderer then works for free**.
4. Register the factory (e.g. `oldms_file.cpp::document()` switches on
Expand Down
197 changes: 197 additions & 0 deletions src/odr/internal/common/element_adapter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#pragma once

#include <odr/definitions.hpp>
#include <odr/document_element.hpp>
#include <odr/document_path.hpp>

#include <odr/internal/abstract/document.hpp>
#include <odr/internal/util/document_util.hpp>

#include <type_traits>

namespace odr::internal {

/// Answers the `*_adapter(id)` hooks for the @p Adapters it is given, and
/// inherits them: a hook whose adapter is in the pack returns `this` for its
/// element type, the rest keep the abstract nullptr.
template <typename... Adapters>
class ElementAdapter : public abstract::ElementAdapter, public Adapters... {
public:
[[nodiscard]] bool element_is_unique(
[[maybe_unused]] const ElementIdentifier element_id) const override {
return true;
}
[[nodiscard]] bool element_is_self_locatable(
[[maybe_unused]] const ElementIdentifier element_id) const override {
return true;
}
[[nodiscard]] bool element_is_editable(
[[maybe_unused]] const ElementIdentifier element_id) const override {
return false;
}
[[nodiscard]] DocumentPath
element_document_path(const ElementIdentifier element_id) const override {
return util::document::extract_path(*this, element_id, null_element_id);
}
[[nodiscard]] ElementIdentifier
element_navigate_path(const ElementIdentifier element_id,
const DocumentPath &path) const override {
return util::document::navigate_path(*this, element_id, path);
}

[[nodiscard]] const abstract::TextRootAdapter *
text_root_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::TextRootAdapter, ElementType::root>(element_id);
}
[[nodiscard]] const abstract::SlideAdapter *
slide_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::SlideAdapter, ElementType::slide>(element_id);
}
[[nodiscard]] const abstract::PageAdapter *
page_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::PageAdapter, ElementType::page>(element_id);
}
[[nodiscard]] const abstract::SheetAdapter *
sheet_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::SheetAdapter, ElementType::sheet>(element_id);
}
[[nodiscard]] const abstract::SheetCellAdapter *
sheet_cell_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::SheetCellAdapter, ElementType::sheet_cell>(
element_id);
}
[[nodiscard]] const abstract::MasterPageAdapter *
master_page_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::MasterPageAdapter, ElementType::master_page>(
element_id);
}
[[nodiscard]] const abstract::LineBreakAdapter *
line_break_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::LineBreakAdapter, ElementType::line_break>(
element_id);
}
[[nodiscard]] const abstract::ParagraphAdapter *
paragraph_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::ParagraphAdapter, ElementType::paragraph>(
element_id);
}
[[nodiscard]] const abstract::SpanAdapter *
span_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::SpanAdapter, ElementType::span>(element_id);
}
[[nodiscard]] const abstract::TextAdapter *
text_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::TextAdapter, ElementType::text>(element_id);
}
[[nodiscard]] const abstract::LinkAdapter *
link_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::LinkAdapter, ElementType::link>(element_id);
}
[[nodiscard]] const abstract::BookmarkAdapter *
bookmark_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::BookmarkAdapter, ElementType::bookmark>(
element_id);
}
[[nodiscard]] const abstract::ListAdapter *
list_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::ListAdapter, ElementType::list>(element_id);
}
[[nodiscard]] const abstract::ListItemAdapter *
list_item_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::ListItemAdapter, ElementType::list_item>(
element_id);
}
[[nodiscard]] const abstract::TableAdapter *
table_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::TableAdapter, ElementType::table>(element_id);
}
[[nodiscard]] const abstract::TableColumnAdapter *
table_column_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::TableColumnAdapter, ElementType::table_column>(
element_id);
}
[[nodiscard]] const abstract::TableRowAdapter *
table_row_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::TableRowAdapter, ElementType::table_row>(
element_id);
}
[[nodiscard]] const abstract::TableCellAdapter *
table_cell_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::TableCellAdapter, ElementType::table_cell>(
element_id);
}
[[nodiscard]] const abstract::FrameAdapter *
frame_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::FrameAdapter, ElementType::frame>(element_id);
}
[[nodiscard]] const abstract::RectAdapter *
rect_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::RectAdapter, ElementType::rect>(element_id);
}
[[nodiscard]] const abstract::LineAdapter *
line_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::LineAdapter, ElementType::line>(element_id);
}
[[nodiscard]] const abstract::CircleAdapter *
circle_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::CircleAdapter, ElementType::circle>(element_id);
}
[[nodiscard]] const abstract::CustomShapeAdapter *
custom_shape_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::CustomShapeAdapter, ElementType::custom_shape>(
element_id);
}
[[nodiscard]] const abstract::ImageAdapter *
image_adapter(const ElementIdentifier element_id) const override {
return adapter_<abstract::ImageAdapter, ElementType::image>(element_id);
}

private:
template <typename Adapter, ElementType type>
[[nodiscard]] const Adapter *
adapter_(const ElementIdentifier element_id) const {
if constexpr ((std::is_same_v<Adapter, Adapters> || ...)) {
return element_type(element_id) == type ? this : nullptr;
} else {
return nullptr;
}
}
};

/// Navigates the tree of a registry whose `element_at(id)` yields the links.
template <typename Registry, typename... Adapters>
class RegistryElementAdapter : public ElementAdapter<Adapters...> {
public:
explicit RegistryElementAdapter(Registry &registry) : m_registry(&registry) {}

[[nodiscard]] ElementType
element_type(const ElementIdentifier element_id) const override {
return m_registry->element_at(element_id).type;
}

[[nodiscard]] ElementIdentifier
element_parent(const ElementIdentifier element_id) const override {
return m_registry->element_at(element_id).parent_id;
}
[[nodiscard]] ElementIdentifier
element_first_child(const ElementIdentifier element_id) const override {
return m_registry->element_at(element_id).first_child_id;
}
[[nodiscard]] ElementIdentifier
element_last_child(const ElementIdentifier element_id) const override {
return m_registry->element_at(element_id).last_child_id;
}
[[nodiscard]] ElementIdentifier
element_previous_sibling(const ElementIdentifier element_id) const override {
return m_registry->element_at(element_id).previous_sibling_id;
}
[[nodiscard]] ElementIdentifier
element_next_sibling(const ElementIdentifier element_id) const override {
return m_registry->element_at(element_id).next_sibling_id;
}

protected:
Registry *m_registry{nullptr};
};

} // namespace odr::internal
Loading
Loading