OpenDocument driver for Pharo
# Pharo OpenDocument (POD)
A free-standing **OpenDocument Format (ODF)** engine for [Pharo](https://pharo.org),
written in pure Pharo Smalltalk. POD reads and writes OASIS OpenDocument files —
starting with **OpenDocument Text (`.odt`)** — as a proper object model, with no
external native dependencies beyond XML parsing/writing.
All classes are prefixed **`POD`**.
> Status: early but working. POD authors `.odt` files that open cleanly in
> LibreOffice, reads existing `.odt` documents back into an editable model, and
> round-trips real LibreOffice documents (preserving the parts it does not model).
## Why
POD was extracted from work on the **PharOffice** suite to give the Pharo community
a reusable, standalone ODF engine. It is intentionally decoupled from any UI so it
can back a word processor, a report generator, a document-conversion service, or a
head-less pipeline.
## Features
- **ODF package layer** — `PODPackage` reads/writes the ODF ZIP container with the
mandatory uncompressed `mimetype` first entry, a spec-correct `META-INF/manifest.xml`,
and pass-through of parts it does not model (round-trip fidelity).
- **Text document model** — `PODTextDocument` with a rich content tree: paragraphs,
headings, styled spans, line breaks/tabs, bullet/number lists, and tables.
- **Styles** — `PODStyle` captures automatic and common styles as ODF
`style:*-properties`, round-tripped without loss of the properties it carries.
- **Metadata** — `PODMetadata` reads/writes `meta.xml` (title, creator, dates, …).
- **Reader** — `PODTextReader` parses `content.xml` into the model; existing
LibreOffice/OpenOffice documents load and re-save.
- **Pharo `Text` bridge (bidirectional)** — `PODRichTextImporter` converts a native
Pharo rich `Text` into an ODF document and `PODPharoTextExporter` /
`PODTextDocument>>asPharoText` converts one back, preserving emphasis
(bold/italic/underline/strikethrough), colour, font family/size, and paragraph
alignment. One-call savers included.
## Quick start
```smalltalk
"Author a document"
| doc |
doc := PODTextDocument new.
doc metadata title: 'My Report'; creator: 'Me'.
doc addHeading: 'Introduction' level: 1.
doc addParagraph: 'The quick brown fox.'.
(doc add: (PODParagraph text: 'Some '))
addSpan: (PODSpan text: 'emphasis' styleName: 'Emphasis').
doc add: (PODList new addText: 'First'; addText: 'Second'; yourself).
doc add: ((PODTable named: 'Grid')
addRowWithTexts: #('A' 'B');
addRowWithTexts: #('C' 'D'); yourself).
doc writeToFile: 'report.odt'.
"Read a document back"
| loaded |
loaded := PODDocument fromFile: 'report.odt'.
loaded text. "=> whole-document plain text"
loaded body. "=> ordered collection of block elements"
"Save a Pharo Text (rich text) as ODF, and load one back"
PODTextDocument saveText: aPharoText titled: 'Export' toFile: 'export.odt'.
(PODDocument fromFile: 'export.odt') asPharoText. "=> a Pharo Text with runs"
```
## Installation
```smalltalk
Metacello new
baseline: 'PharoOpenDocument';
repository: 'github://riverdusty/Pharo-OpenDocument:main/src';
load.
```
Dependencies (`XML-Parser`, `XML-Writer`) are declared in the baseline and loaded
automatically.
## Architecture
| Package | Contents |
|---|---|
| `PharoOpenDocument-Core` | `PODPackage`, `PODManifest(+Entry)`, `PODMetadata`, `PODNamespaces`, abstract `PODDocument` |
| `PharoOpenDocument-Text` | Content model (`PODElement` tree), `PODStyle`, `PODTextDocument`, `PODTextReader`, `PODRichTextImporter`, `PODPharoTextExporter` |
| `PharoOpenDocument-Tests` | SUnit suite |
Content elements serialise themselves (`writeXmlOn:`); the reader centralises parsing
and dispatch. A document keeps its source `PODPackage`, so unmodeled parts
(`styles.xml`, `settings.xml`, …) survive a read-modify-write cycle byte-for-byte.
## ODF conformance notes
Learned by validating against LibreOffice and baked into the engine:
- **`mimetype` must be the first ZIP entry and stored (uncompressed)** — enforced by
`PODPackage`.
- **Manifest `full-path` values are relative** (`content.xml`), never `/content.xml`;
only the whole-document entry is `/`. A leading slash makes LibreOffice reject the
package as corrupt.
- **`office:version`** is emitted from a single configurable source
(`PODNamespaces class >> odfVersion`, default `1.4`). Current LibreOffice validates
the declared version strictly.
- **`content.xml` is not pretty-printed** — `text:p`/`text:h`/`text:span` are mixed
content, so indentation whitespace would become literal document text.
## Development
The engine is built and verified against a live Pharo image over
[SmalltalkGenie](https://github.com/KentBeck/SmalltalkGenie). The `tools/` directory
holds the reproducible build drivers (`build_*.py`), the Tonel exporter (`export.py`),
and the Genie helper (`genie.py`). `samples/` holds generated example documents.
## Roadmap
- Spreadsheet (`.ods`) and presentation (`.odp`) document types
- Richer style modelling (page layouts, list styles, table styles)
- Images and embedded objects (binary parts as assets)
- Full ODF whitespace-normalisation on read for foreign pretty-printed files
## License
MIT (intended for contribution to the Pharo community).