odt2web 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odt2web-0.8.0/PKG-INFO +400 -0
- odt2web-0.8.0/README.md +380 -0
- odt2web-0.8.0/pyproject.toml +41 -0
- odt2web-0.8.0/setup.cfg +4 -0
- odt2web-0.8.0/src/odt2web/__init__.py +25 -0
- odt2web-0.8.0/src/odt2web/__main__.py +4 -0
- odt2web-0.8.0/src/odt2web/assets.py +82 -0
- odt2web-0.8.0/src/odt2web/cli.py +139 -0
- odt2web-0.8.0/src/odt2web/codeblocks.py +87 -0
- odt2web-0.8.0/src/odt2web/config.py +74 -0
- odt2web-0.8.0/src/odt2web/convert.py +186 -0
- odt2web-0.8.0/src/odt2web/css.py +195 -0
- odt2web-0.8.0/src/odt2web/extensibility.py +57 -0
- odt2web-0.8.0/src/odt2web/frontmatter.py +103 -0
- odt2web-0.8.0/src/odt2web/metadata.py +45 -0
- odt2web-0.8.0/src/odt2web/model.py +207 -0
- odt2web-0.8.0/src/odt2web/ns.py +29 -0
- odt2web-0.8.0/src/odt2web/parser.py +560 -0
- odt2web-0.8.0/src/odt2web/rawhtml.py +93 -0
- odt2web-0.8.0/src/odt2web/reader.py +93 -0
- odt2web-0.8.0/src/odt2web/renderer.py +438 -0
- odt2web-0.8.0/src/odt2web/result.py +67 -0
- odt2web-0.8.0/src/odt2web/security.py +78 -0
- odt2web-0.8.0/src/odt2web/styles.py +410 -0
- odt2web-0.8.0/src/odt2web/tables.py +46 -0
- odt2web-0.8.0/src/odt2web.egg-info/PKG-INFO +400 -0
- odt2web-0.8.0/src/odt2web.egg-info/SOURCES.txt +29 -0
- odt2web-0.8.0/src/odt2web.egg-info/dependency_links.txt +1 -0
- odt2web-0.8.0/src/odt2web.egg-info/entry_points.txt +2 -0
- odt2web-0.8.0/src/odt2web.egg-info/requires.txt +1 -0
- odt2web-0.8.0/src/odt2web.egg-info/top_level.txt +1 -0
odt2web-0.8.0/PKG-INFO
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odt2web
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Converts OpenDocument Text (.odt) documents to semantic HTML + CSS.
|
|
5
|
+
Author-email: "Diego C. Sampaio" <slavelix@ymail.com>
|
|
6
|
+
License-Expression: AGPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/kriptolix/Asteria-Project
|
|
8
|
+
Project-URL: Repository, https://github.com/kriptolix/Asteria-Project
|
|
9
|
+
Project-URL: Issues, https://github.com/kriptolix/Asteria-Project/issues
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Topic :: Documentation
|
|
16
|
+
Classifier: Topic :: Text Processing
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: PyYAML>=6.0
|
|
20
|
+
|
|
21
|
+
# odt2web
|
|
22
|
+
|
|
23
|
+
Python library for converting OpenDocument Text (`.odt`) documents into
|
|
24
|
+
semantic HTML, with optional CSS and image extraction.
|
|
25
|
+
|
|
26
|
+
Preserves the semantic and structural intent of the document, not its
|
|
27
|
+
printed-page appearance.
|
|
28
|
+
|
|
29
|
+
## Table of contents
|
|
30
|
+
|
|
31
|
+
- [Installation](#installation)
|
|
32
|
+
- [Basic usage](#basic-usage)
|
|
33
|
+
- [`ConversionResult`](#conversionresult)
|
|
34
|
+
- [Architecture](#architecture)
|
|
35
|
+
- [Style system](#style-system)
|
|
36
|
+
- [Code blocks](#code-blocks)
|
|
37
|
+
- [Columns](#columns)
|
|
38
|
+
- [Assets](#assets)
|
|
39
|
+
- [Metadata](#metadata)
|
|
40
|
+
- [Front matter (SSGs)](#front-matter-ssgs)
|
|
41
|
+
- [Embedded raw HTML (`:::html`)](#embedded-raw-html-html)
|
|
42
|
+
- [Security](#security)
|
|
43
|
+
- [CLI](#cli)
|
|
44
|
+
- [Configuration (YAML)](#configuration-yaml)
|
|
45
|
+
- [API reference](#api-reference)
|
|
46
|
+
- [Extensibility](#extensibility)
|
|
47
|
+
- [Tests](#tests)
|
|
48
|
+
- [Known limitations](#known-limitations)
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Basic usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from odt2web import convert
|
|
60
|
+
|
|
61
|
+
result = convert("article.odt")
|
|
62
|
+
print(result.html)
|
|
63
|
+
print(result.css)
|
|
64
|
+
|
|
65
|
+
result = convert(
|
|
66
|
+
"article.odt",
|
|
67
|
+
output_dir="dist/",
|
|
68
|
+
css=True,
|
|
69
|
+
extract_images=True,
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
From bytes:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from odt2web import convert_bytes
|
|
77
|
+
|
|
78
|
+
result = convert_bytes(odt_bytes)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## `ConversionResult`
|
|
82
|
+
|
|
83
|
+
| Attribute | Type | Description |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `result.html` | `str` | The generated HTML. |
|
|
86
|
+
| `result.css` | `str \| None` | Generated CSS, if `css=True`. |
|
|
87
|
+
| `result.assets` | `list[Asset]` | Extracted images/assets. |
|
|
88
|
+
| `result.metadata` | `Metadata` | Document metadata (title, author, description — see [Metadata](#metadata)). |
|
|
89
|
+
| `result.front_matter` | `dict \| None` | Parsed front matter block, if present — see [Front matter](#front-matter-ssgs). |
|
|
90
|
+
| `result.warnings` | `list[Warning]` | Non-fatal issues found during conversion. |
|
|
91
|
+
|
|
92
|
+
## Architecture
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
ODT → Package Reader → Document Model → Semantic Renderer → HTML + CSS
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- **Package Reader** (`odt2web.reader`): reads the `.odt` as a ZIP
|
|
99
|
+
(`content.xml`, `styles.xml`, `meta.xml`, `manifest.xml`, `Pictures/`),
|
|
100
|
+
without depending on LibreOffice.
|
|
101
|
+
- **Document Model** (`odt2web.model`): a custom intermediate representation
|
|
102
|
+
(`Document`, `Section`, `Heading`, `Paragraph`, `Table`, ...), decoupled
|
|
103
|
+
from any output format.
|
|
104
|
+
- **Semantic Renderer** (`odt2web.renderer`, `odt2web.css`): converts the
|
|
105
|
+
model into HTML and generates only the CSS required by the resources
|
|
106
|
+
actually used.
|
|
107
|
+
|
|
108
|
+
## Style system
|
|
109
|
+
|
|
110
|
+
Configurable default mapping:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
style_map = {
|
|
114
|
+
"Title": "h1",
|
|
115
|
+
"Heading 1": "h2",
|
|
116
|
+
"Heading 2": "h3",
|
|
117
|
+
"Heading 3": "h4",
|
|
118
|
+
"Quotation": "blockquote",
|
|
119
|
+
"Text Body": "p",
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Custom styles (tag + class):
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
style_map = {
|
|
127
|
+
"Lead": {"tag": "p", "class": "lead"},
|
|
128
|
+
"Warning": {"tag": "aside", "class": "warning"},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
result = convert("article.odt", style_map=style_map)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Style resolution follows the ODT style inheritance chain (a paragraph often
|
|
135
|
+
uses an automatic style generated by the editor, e.g. `P1`, whose parent is
|
|
136
|
+
the actual named style) and matches against `style_map` first, then against
|
|
137
|
+
the default mapping above.
|
|
138
|
+
|
|
139
|
+
## Code blocks
|
|
140
|
+
|
|
141
|
+
For code blocks (consecutive paragraphs using LibreOffice's "Preformatted
|
|
142
|
+
Text" style, or any style mapped to `pre`), the library:
|
|
143
|
+
|
|
144
|
+
- merges consecutive paragraphs into a single block instead of creating one
|
|
145
|
+
`<pre>` per line;
|
|
146
|
+
- generates `<pre><code class="language-xxx">...</code></pre>` — the markup
|
|
147
|
+
expected by highlighters;
|
|
148
|
+
- does not apply bold/italic/etc. formatting to the content — inline
|
|
149
|
+
formatting inside code would break copy-paste and highlighter
|
|
150
|
+
tokenization.
|
|
151
|
+
|
|
152
|
+
The library itself does not perform syntax highlighting — that is handled
|
|
153
|
+
by the highlighter you already use on your website (client-side or during
|
|
154
|
+
the SSG build). The library's job is simply to provide HTML in the correct
|
|
155
|
+
format.
|
|
156
|
+
|
|
157
|
+
The language of a code block can be specified in two ways:
|
|
158
|
+
|
|
159
|
+
1. **Marker at the beginning of the block** (Markdown-style): fencing the
|
|
160
|
+
block with an opening line of three backticks followed by the language
|
|
161
|
+
name (e.g. `python`) defines the language and is removed from the final
|
|
162
|
+
HTML. This does not require creating any new style in the ODT.
|
|
163
|
+
2. **Class in `style_map`**: if you prefer dedicated ODT styles for each
|
|
164
|
+
language (e.g. "Code Python", "Code JS"), map each one to a
|
|
165
|
+
`language-xxx` class:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
style_map = {
|
|
169
|
+
"Code Python": {"tag": "pre", "class": "language-python"},
|
|
170
|
+
"Code JS": {"tag": "pre", "class": "language-javascript"},
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Inline code (a word or short snippet within a paragraph) uses LibreOffice's
|
|
175
|
+
native "Source Text" character style and is automatically converted to
|
|
176
|
+
`<code>...</code>`, with no additional configuration.
|
|
177
|
+
|
|
178
|
+
## Columns
|
|
179
|
+
|
|
180
|
+
ODT sections with multiple columns generate:
|
|
181
|
+
|
|
182
|
+
```html
|
|
183
|
+
<section class="odt-columns odt-columns-2">...</section>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```css
|
|
187
|
+
.odt-columns-2 {
|
|
188
|
+
column-count: 2;
|
|
189
|
+
column-gap: 1.27cm;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The number of columns and the spacing are read directly from the ODT; text
|
|
194
|
+
distribution is delegated to CSS (`column-count`) and is never calculated
|
|
195
|
+
manually.
|
|
196
|
+
|
|
197
|
+
## Assets
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
result = convert("article.odt", assets_dir="assets/")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
dist/
|
|
205
|
+
├── index.html
|
|
206
|
+
├── style.css
|
|
207
|
+
└── assets/
|
|
208
|
+
├── image-001.png
|
|
209
|
+
└── image-002.jpg
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Metadata
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
result.metadata.title
|
|
216
|
+
result.metadata.author
|
|
217
|
+
result.metadata.description
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Front matter (SSGs)
|
|
221
|
+
|
|
222
|
+
If the document starts with a block delimited by `---`, in the following
|
|
223
|
+
format:
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
---
|
|
227
|
+
title: My First Article
|
|
228
|
+
date: 2026-08-20
|
|
229
|
+
author: Author
|
|
230
|
+
tags: Python, Web
|
|
231
|
+
categories: Technology
|
|
232
|
+
---
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
(either as consecutive paragraphs — one per line — or as a single paragraph
|
|
236
|
+
with manual line breaks inserted using Shift+Enter), it is recognized,
|
|
237
|
+
removed from the normal text flow, and converted to:
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
<div class="ssg-frontmatter" data-ssg="frontmatter">
|
|
241
|
+
<meta data-key="title" content="My First Article">
|
|
242
|
+
<meta data-key="date" content="2026-08-20">
|
|
243
|
+
<meta data-key="author" content="Author">
|
|
244
|
+
<meta data-key="tags" content="Python, Web">
|
|
245
|
+
<meta data-key="categories" content="Technology">
|
|
246
|
+
</div>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
It is always placed as the first content in the generated HTML. The
|
|
250
|
+
key-value pairs are also available in `result.front_matter` (`dict`). If
|
|
251
|
+
there is no block, the value is `None` and nothing is added to the HTML.
|
|
252
|
+
Empty paragraphs and page breaks preceding the block (common in editor
|
|
253
|
+
templates) do not prevent detection.
|
|
254
|
+
|
|
255
|
+
## Embedded raw HTML (`:::html`)
|
|
256
|
+
|
|
257
|
+
For embeds (iframes, third-party widgets, etc.), instead of introducing a
|
|
258
|
+
syntax for each individual feature, the library provides a generic escape
|
|
259
|
+
hatch: a literal HTML block delimited by markers, written directly in the
|
|
260
|
+
`.odt`:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
:::html
|
|
264
|
+
<iframe src="https://example.com/map" width="600" height="400"></iframe>
|
|
265
|
+
:::
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
(This works both as consecutive paragraphs and as a single paragraph with
|
|
269
|
+
manual line breaks, anywhere in the document.)
|
|
270
|
+
|
|
271
|
+
It is passed through without escaping, but this is opt-in:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
result = convert("article.odt", allow_raw_html=True)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
By default (`allow_raw_html=False`), the block appears escaped as literal
|
|
278
|
+
text instead of being executed as HTML, with a warning in
|
|
279
|
+
`result.warnings` — the `.odt` is treated as untrusted input by default
|
|
280
|
+
(see [Security](#security)). Only enable this for documents from authors
|
|
281
|
+
you trust.
|
|
282
|
+
|
|
283
|
+
The content undergoes basic typographic normalization (curly quotes →
|
|
284
|
+
straight quotes) before becoming HTML, since LibreOffice's autocorrect
|
|
285
|
+
often changes `"` to `“`/`”`, which would break attributes such as
|
|
286
|
+
`src="..."`.
|
|
287
|
+
|
|
288
|
+
In the CLI: `odt2web article.odt --allow-raw-html`. In `config.yaml`:
|
|
289
|
+
`output.allow_raw_html: true`.
|
|
290
|
+
|
|
291
|
+
## Security
|
|
292
|
+
|
|
293
|
+
The `.odt` is treated as untrusted input (`odt2web.security`): text is
|
|
294
|
+
always escaped; URLs (`href`, referenced images) are checked against an
|
|
295
|
+
allowlist of permitted schemes (`http`, `https`, `mailto`, `tel`, relative
|
|
296
|
+
URLs) — schemes such as `javascript:` or `file:` are removed.
|
|
297
|
+
|
|
298
|
+
No macros or embedded ODT content are executed. The only escape hatch that
|
|
299
|
+
emits unescaped content is the `:::html` block (above), and only when
|
|
300
|
+
explicitly enabled.
|
|
301
|
+
|
|
302
|
+
## CLI
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
odt2web article.odt
|
|
306
|
+
odt2web article.odt -o dist/
|
|
307
|
+
odt2web article.odt --css
|
|
308
|
+
odt2web article.odt --assets assets/
|
|
309
|
+
odt2web article.odt --fragment
|
|
310
|
+
odt2web article.odt --allow-raw-html
|
|
311
|
+
odt2web article.odt --config config.yaml
|
|
312
|
+
odt2web --check article.odt
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
CLI default: complete HTML document (use `--fragment` to generate only the
|
|
316
|
+
fragment). In the Python API it is the opposite — see `document=` below.
|
|
317
|
+
|
|
318
|
+
## Configuration (YAML)
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
output:
|
|
322
|
+
document: false
|
|
323
|
+
css: true
|
|
324
|
+
assets: assets
|
|
325
|
+
allow_raw_html: false
|
|
326
|
+
|
|
327
|
+
columns:
|
|
328
|
+
enabled: true
|
|
329
|
+
|
|
330
|
+
styles:
|
|
331
|
+
Title: h1
|
|
332
|
+
Heading 1: h2
|
|
333
|
+
Heading 2: h3
|
|
334
|
+
Quotation: blockquote
|
|
335
|
+
Lead:
|
|
336
|
+
tag: p
|
|
337
|
+
class: lead
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
convert("article.odt", config="config.yaml")
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Priority: explicit argument passed to the call > configuration file >
|
|
345
|
+
library default.
|
|
346
|
+
|
|
347
|
+
## API reference
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
convert(
|
|
351
|
+
path,
|
|
352
|
+
*,
|
|
353
|
+
output_dir=None, # writes index.html/style.css/assets to this directory
|
|
354
|
+
css=True, # automatically generates CSS
|
|
355
|
+
extract_images=True, # extracts embedded images into separate files
|
|
356
|
+
assets_dir="assets", # subdirectory (relative to the HTML) for assets
|
|
357
|
+
document=False, # True = complete HTML document; False = fragment
|
|
358
|
+
style_map=None, # ODT style -> HTML mapping
|
|
359
|
+
custom_renderers=None, # additional renderers beyond those registered via @register_renderer
|
|
360
|
+
allow_raw_html=False, # enables :::html blocks without escaping
|
|
361
|
+
config=None, # path to a YAML configuration file
|
|
362
|
+
)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
- **`convert_bytes(data, **same_kwargs)`** — same functionality, using
|
|
366
|
+
in-memory bytes.
|
|
367
|
+
- **`check(path)`** — validates the `.odt` without generating HTML; returns
|
|
368
|
+
`list[Warning]` and raises `InvalidOdtError` if the package cannot be
|
|
369
|
+
read.
|
|
370
|
+
|
|
371
|
+
## Extensibility
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from odt2web import register_renderer
|
|
375
|
+
from odt2web.renderer import render_inline
|
|
376
|
+
|
|
377
|
+
@register_renderer("custom:note")
|
|
378
|
+
def render_note(node, context):
|
|
379
|
+
return f"<aside class='note'>{render_inline(node.children, context)}</aside>"
|
|
380
|
+
|
|
381
|
+
style_map = {"Warning": {"tag": "custom:note"}}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
A complete list of available CSS classes can be found [here](DOCS.md).
|
|
385
|
+
|
|
386
|
+
## Known limitations
|
|
387
|
+
|
|
388
|
+
- Front matter, `:::html` blocks, and code block merging operate at the
|
|
389
|
+
document's "loose" level (outside lists/tables); inside a table cell or
|
|
390
|
+
list item, each paragraph continues to be processed individually.
|
|
391
|
+
- Table column widths and endnotes separated from footnotes are not implemented — the architecture (an intermediate model independent of HTML) already supports these without
|
|
392
|
+
structural changes.
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
AGPL-3.0.
|
|
397
|
+
|
|
398
|
+
## Contributing
|
|
399
|
+
|
|
400
|
+
Issues and pull requests are welcome.
|