Metadata-Version: 2.4
Name: odt2web
Version: 0.8.0
Summary: Converts OpenDocument Text (.odt) documents to semantic HTML + CSS.
Author-email: "Diego C. Sampaio" <slavelix@ymail.com>
License-Expression: AGPL-3.0-only
Project-URL: Homepage, https://github.com/kriptolix/Asteria-Project
Project-URL: Repository, https://github.com/kriptolix/Asteria-Project
Project-URL: Issues, https://github.com/kriptolix/Asteria-Project/issues
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Documentation
Classifier: Topic :: Text Processing
Requires-Python: >=3.13
Description-Content-Type: text/markdown
Requires-Dist: PyYAML>=6.0

# odt2web

Python library for converting OpenDocument Text (`.odt`) documents into
semantic HTML, with optional CSS and image extraction.

Preserves the semantic and structural intent of the document, not its
printed-page appearance.

## Table of contents

- [Installation](#installation)
- [Basic usage](#basic-usage)
- [`ConversionResult`](#conversionresult)
- [Architecture](#architecture)
- [Style system](#style-system)
- [Code blocks](#code-blocks)
- [Columns](#columns)
- [Assets](#assets)
- [Metadata](#metadata)
- [Front matter (SSGs)](#front-matter-ssgs)
- [Embedded raw HTML (`:::html`)](#embedded-raw-html-html)
- [Security](#security)
- [CLI](#cli)
- [Configuration (YAML)](#configuration-yaml)
- [API reference](#api-reference)
- [Extensibility](#extensibility)
- [Tests](#tests)
- [Known limitations](#known-limitations)

## Installation

```bash
pip install .
```

## Basic usage

```python
from odt2web import convert

result = convert("article.odt")
print(result.html)
print(result.css)

result = convert(
    "article.odt",
    output_dir="dist/",
    css=True,
    extract_images=True,
)
```

From bytes:

```python
from odt2web import convert_bytes

result = convert_bytes(odt_bytes)
```

## `ConversionResult`

| Attribute | Type | Description |
|---|---|---|
| `result.html` | `str` | The generated HTML. |
| `result.css` | `str \| None` | Generated CSS, if `css=True`. |
| `result.assets` | `list[Asset]` | Extracted images/assets. |
| `result.metadata` | `Metadata` | Document metadata (title, author, description — see [Metadata](#metadata)). |
| `result.front_matter` | `dict \| None` | Parsed front matter block, if present — see [Front matter](#front-matter-ssgs). |
| `result.warnings` | `list[Warning]` | Non-fatal issues found during conversion. |

## Architecture

```
ODT → Package Reader → Document Model → Semantic Renderer → HTML + CSS
```

- **Package Reader** (`odt2web.reader`): reads the `.odt` as a ZIP
  (`content.xml`, `styles.xml`, `meta.xml`, `manifest.xml`, `Pictures/`),
  without depending on LibreOffice.
- **Document Model** (`odt2web.model`): a custom intermediate representation
  (`Document`, `Section`, `Heading`, `Paragraph`, `Table`, ...), decoupled
  from any output format.
- **Semantic Renderer** (`odt2web.renderer`, `odt2web.css`): converts the
  model into HTML and generates only the CSS required by the resources
  actually used.

## Style system

Configurable default mapping:

```python
style_map = {
    "Title": "h1",
    "Heading 1": "h2",
    "Heading 2": "h3",
    "Heading 3": "h4",
    "Quotation": "blockquote",
    "Text Body": "p",
}
```

Custom styles (tag + class):

```python
style_map = {
    "Lead": {"tag": "p", "class": "lead"},
    "Warning": {"tag": "aside", "class": "warning"},
}

result = convert("article.odt", style_map=style_map)
```

Style resolution follows the ODT style inheritance chain (a paragraph often
uses an automatic style generated by the editor, e.g. `P1`, whose parent is
the actual named style) and matches against `style_map` first, then against
the default mapping above.

## Code blocks

For code blocks (consecutive paragraphs using LibreOffice's "Preformatted
Text" style, or any style mapped to `pre`), the library:

- merges consecutive paragraphs into a single block instead of creating one
  `<pre>` per line;
- generates `<pre><code class="language-xxx">...</code></pre>` — the markup
  expected by highlighters;
- does not apply bold/italic/etc. formatting to the content — inline
  formatting inside code would break copy-paste and highlighter
  tokenization.

The library itself does not perform syntax highlighting — that is handled
by the highlighter you already use on your website (client-side or during
the SSG build). The library's job is simply to provide HTML in the correct
format.

The language of a code block can be specified in two ways:

1. **Marker at the beginning of the block** (Markdown-style): fencing the
   block with an opening line of three backticks followed by the language
   name (e.g. `python`) defines the language and is removed from the final
   HTML. This does not require creating any new style in the ODT.
2. **Class in `style_map`**: if you prefer dedicated ODT styles for each
   language (e.g. "Code Python", "Code JS"), map each one to a
   `language-xxx` class:

   ```python
   style_map = {
       "Code Python": {"tag": "pre", "class": "language-python"},
       "Code JS": {"tag": "pre", "class": "language-javascript"},
   }
   ```

Inline code (a word or short snippet within a paragraph) uses LibreOffice's
native "Source Text" character style and is automatically converted to
`<code>...</code>`, with no additional configuration.

## Columns

ODT sections with multiple columns generate:

```html
<section class="odt-columns odt-columns-2">...</section>
```

```css
.odt-columns-2 {
  column-count: 2;
  column-gap: 1.27cm;
}
```

The number of columns and the spacing are read directly from the ODT; text
distribution is delegated to CSS (`column-count`) and is never calculated
manually.

## Assets

```python
result = convert("article.odt", assets_dir="assets/")
```

```
dist/
├── index.html
├── style.css
└── assets/
    ├── image-001.png
    └── image-002.jpg
```

## Metadata

```python
result.metadata.title
result.metadata.author
result.metadata.description
```

## Front matter (SSGs)

If the document starts with a block delimited by `---`, in the following
format:

```yaml
---
title: My First Article
date: 2026-08-20
author: Author
tags: Python, Web
categories: Technology
---
```

(either as consecutive paragraphs — one per line — or as a single paragraph
with manual line breaks inserted using Shift+Enter), it is recognized,
removed from the normal text flow, and converted to:

```html
<div class="ssg-frontmatter" data-ssg="frontmatter">
  <meta data-key="title" content="My First Article">
  <meta data-key="date" content="2026-08-20">
  <meta data-key="author" content="Author">
  <meta data-key="tags" content="Python, Web">
  <meta data-key="categories" content="Technology">
</div>
```

It is always placed as the first content in the generated HTML. The
key-value pairs are also available in `result.front_matter` (`dict`). If
there is no block, the value is `None` and nothing is added to the HTML.
Empty paragraphs and page breaks preceding the block (common in editor
templates) do not prevent detection.

## Embedded raw HTML (`:::html`)

For embeds (iframes, third-party widgets, etc.), instead of introducing a
syntax for each individual feature, the library provides a generic escape
hatch: a literal HTML block delimited by markers, written directly in the
`.odt`:

```
:::html
<iframe src="https://example.com/map" width="600" height="400"></iframe>
:::
```

(This works both as consecutive paragraphs and as a single paragraph with
manual line breaks, anywhere in the document.)

It is passed through without escaping, but this is opt-in:

```python
result = convert("article.odt", allow_raw_html=True)
```

By default (`allow_raw_html=False`), the block appears escaped as literal
text instead of being executed as HTML, with a warning in
`result.warnings` — the `.odt` is treated as untrusted input by default
(see [Security](#security)). Only enable this for documents from authors
you trust.

The content undergoes basic typographic normalization (curly quotes →
straight quotes) before becoming HTML, since LibreOffice's autocorrect
often changes `"` to `“`/`”`, which would break attributes such as
`src="..."`.

In the CLI: `odt2web article.odt --allow-raw-html`. In `config.yaml`:
`output.allow_raw_html: true`.

## Security

The `.odt` is treated as untrusted input (`odt2web.security`): text is
always escaped; URLs (`href`, referenced images) are checked against an
allowlist of permitted schemes (`http`, `https`, `mailto`, `tel`, relative
URLs) — schemes such as `javascript:` or `file:` are removed.

No macros or embedded ODT content are executed. The only escape hatch that
emits unescaped content is the `:::html` block (above), and only when
explicitly enabled.

## CLI

```bash
odt2web article.odt
odt2web article.odt -o dist/
odt2web article.odt --css
odt2web article.odt --assets assets/
odt2web article.odt --fragment
odt2web article.odt --allow-raw-html
odt2web article.odt --config config.yaml
odt2web --check article.odt
```

CLI default: complete HTML document (use `--fragment` to generate only the
fragment). In the Python API it is the opposite — see `document=` below.

## Configuration (YAML)

```yaml
output:
  document: false
  css: true
  assets: assets
  allow_raw_html: false

columns:
  enabled: true

styles:
  Title: h1
  Heading 1: h2
  Heading 2: h3
  Quotation: blockquote
  Lead:
    tag: p
    class: lead
```

```python
convert("article.odt", config="config.yaml")
```

Priority: explicit argument passed to the call > configuration file >
library default.

## API reference

```python
convert(
    path,
    *,
    output_dir=None,      # writes index.html/style.css/assets to this directory
    css=True,              # automatically generates CSS
    extract_images=True,   # extracts embedded images into separate files
    assets_dir="assets",   # subdirectory (relative to the HTML) for assets
    document=False,        # True = complete HTML document; False = fragment
    style_map=None,        # ODT style -> HTML mapping
    custom_renderers=None, # additional renderers beyond those registered via @register_renderer
    allow_raw_html=False,  # enables :::html blocks without escaping
    config=None,           # path to a YAML configuration file
)
```

- **`convert_bytes(data, **same_kwargs)`** — same functionality, using
  in-memory bytes.
- **`check(path)`** — validates the `.odt` without generating HTML; returns
  `list[Warning]` and raises `InvalidOdtError` if the package cannot be
  read.

## Extensibility

```python
from odt2web import register_renderer
from odt2web.renderer import render_inline

@register_renderer("custom:note")
def render_note(node, context):
    return f"<aside class='note'>{render_inline(node.children, context)}</aside>"

style_map = {"Warning": {"tag": "custom:note"}}
```

A complete list of available CSS classes can be found [here](DOCS.md). 

## Known limitations

- Front matter, `:::html` blocks, and code block merging operate at the
  document's "loose" level (outside lists/tables); inside a table cell or
  list item, each paragraph continues to be processed individually.
- Table column widths and endnotes separated from footnotes are not implemented — the  architecture (an intermediate model independent of HTML) already supports these without
  structural changes.

## License

AGPL-3.0.

## Contributing

Issues and pull requests are welcome.
