epub-generator 0.1.1__tar.gz → 0.1.3__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.
Files changed (26) hide show
  1. {epub_generator-0.1.1 → epub_generator-0.1.3}/PKG-INFO +105 -31
  2. {epub_generator-0.1.1 → epub_generator-0.1.3}/README.md +104 -30
  3. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/__init__.py +4 -2
  4. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/data/style.css.jinja +12 -0
  5. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/generation/gen_asset.py +22 -8
  6. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/generation/gen_chapter.py +34 -7
  7. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/generation/gen_epub.py +16 -2
  8. epub_generator-0.1.3/epub_generator/generation/xml_utils.py +31 -0
  9. epub_generator-0.1.3/epub_generator/html_tag.py +11 -0
  10. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/types.py +21 -9
  11. {epub_generator-0.1.1 → epub_generator-0.1.3}/pyproject.toml +1 -1
  12. epub_generator-0.1.1/epub_generator/generation/xml_utils.py +0 -18
  13. {epub_generator-0.1.1 → epub_generator-0.1.3}/LICENSE +0 -0
  14. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/context.py +0 -0
  15. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/data/container.xml.jinja +0 -0
  16. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/data/content.opf.jinja +0 -0
  17. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/data/cover.xhtml.jinja +0 -0
  18. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/data/mimetype.jinja +0 -0
  19. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/data/nav.xhtml.jinja +0 -0
  20. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/data/part.xhtml.jinja +0 -0
  21. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/generation/__init__.py +0 -0
  22. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/generation/gen_nav.py +0 -0
  23. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/generation/gen_toc.py +0 -0
  24. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/i18n.py +0 -0
  25. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/options.py +0 -0
  26. {epub_generator-0.1.1 → epub_generator-0.1.3}/epub_generator/template.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: epub-generator
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A simple Python EPUB 3.0 generator with a single API call
5
5
  License: MIT
6
6
  Keywords: epub,epub3,ebook,generator,publishing
@@ -48,7 +48,7 @@ pip install epub-generator
48
48
  ### Generate Your First Book in 5 Minutes
49
49
 
50
50
  ```python
51
- from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, Text, TextKind
51
+ from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, TextBlock, TextKind
52
52
 
53
53
  # Prepare book data
54
54
  epub_data = EpubData(
@@ -61,9 +61,9 @@ epub_data = EpubData(
61
61
  title="Chapter 1",
62
62
  get_chapter=lambda: Chapter(
63
63
  elements=[
64
- Text(kind=TextKind.HEADLINE, content=["Chapter 1"]),
65
- Text(kind=TextKind.BODY, content=["This is the first paragraph."]),
66
- Text(kind=TextKind.BODY, content=["This is the second paragraph."]),
64
+ TextBlock(kind=TextKind.HEADLINE, content=["Chapter 1"]),
65
+ TextBlock(kind=TextKind.BODY, content=["This is the first paragraph."]),
66
+ TextBlock(kind=TextKind.BODY, content=["This is the second paragraph."]),
67
67
  ]
68
68
  ),
69
69
  ),
@@ -80,9 +80,9 @@ That's it! You now have a valid EPUB 3.0 ebook file.
80
80
 
81
81
  - **Minimal API**: Just one function call `generate_epub()`
82
82
  - **EPUB 3.0**: Generates standards-compliant EPUB 3.0 format
83
- - **Rich Content**: Supports text, images, tables, math formulas, footnotes
83
+ - **Rich Content**: Supports text, images, tables, math formulas (block-level and inline), footnotes, custom HTML tags
84
84
  - **Flexible Structure**: Nested chapters, prefaces, cover images
85
- - **Math Support**: LaTeX to MathML conversion
85
+ - **Math Support**: LaTeX to MathML/SVG conversion with inline formula support
86
86
  - **Type Safe**: Full type annotations included
87
87
 
88
88
  ## Advanced Usage
@@ -92,7 +92,7 @@ That's it! You now have a valid EPUB 3.0 ebook file.
92
92
  ```python
93
93
  from datetime import datetime, timezone
94
94
  from pathlib import Path
95
- from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, Text, TextKind
95
+ from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, TextBlock, TextKind
96
96
 
97
97
  epub_data = EpubData(
98
98
  meta=BookMeta(
@@ -111,8 +111,8 @@ epub_data = EpubData(
111
111
  title="Chapter 1",
112
112
  get_chapter=lambda: Chapter(
113
113
  elements=[
114
- Text(kind=TextKind.HEADLINE, content=["Chapter 1"]),
115
- Text(kind=TextKind.BODY, content=["Main content..."]),
114
+ TextBlock(kind=TextKind.HEADLINE, content=["Chapter 1"]),
115
+ TextBlock(kind=TextKind.BODY, content=["Main content..."]),
116
116
  ]
117
117
  ),
118
118
  ),
@@ -125,7 +125,7 @@ generate_epub(epub_data, "book_with_cover.epub")
125
125
  ### Nested Chapter Structure
126
126
 
127
127
  ```python
128
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind
128
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind
129
129
 
130
130
  epub_data = EpubData(
131
131
  chapters=[
@@ -136,7 +136,7 @@ epub_data = EpubData(
136
136
  title="Chapter 1.1",
137
137
  get_chapter=lambda: Chapter(
138
138
  elements=[
139
- Text(kind=TextKind.BODY, content=["Content 1.1..."]),
139
+ TextBlock(kind=TextKind.BODY, content=["Content 1.1..."]),
140
140
  ]
141
141
  ),
142
142
  ),
@@ -144,7 +144,7 @@ epub_data = EpubData(
144
144
  title="Chapter 1.2",
145
145
  get_chapter=lambda: Chapter(
146
146
  elements=[
147
- Text(kind=TextKind.BODY, content=["Content 1.2..."]),
147
+ TextBlock(kind=TextKind.BODY, content=["Content 1.2..."]),
148
148
  ]
149
149
  ),
150
150
  ),
@@ -160,7 +160,7 @@ generate_epub(epub_data, "book_with_nested_chapters.epub")
160
160
 
161
161
  ```python
162
162
  from pathlib import Path
163
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Image
163
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, Image
164
164
 
165
165
  epub_data = EpubData(
166
166
  chapters=[
@@ -168,7 +168,7 @@ epub_data = EpubData(
168
168
  title="Chapter 1",
169
169
  get_chapter=lambda: Chapter(
170
170
  elements=[
171
- Text(kind=TextKind.BODY, content=["Here's an image:"]),
171
+ TextBlock(kind=TextKind.BODY, content=["Here's an image:"]),
172
172
  Image(
173
173
  path=Path("image.png"), # Image path
174
174
  alt_text="Image description",
@@ -185,7 +185,7 @@ generate_epub(epub_data, "book_with_images.epub")
185
185
  ### Add Footnotes
186
186
 
187
187
  ```python
188
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Mark, Footnote
188
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, Mark, Footnote
189
189
 
190
190
  epub_data = EpubData(
191
191
  chapters=[
@@ -193,7 +193,7 @@ epub_data = EpubData(
193
193
  title="Chapter 1",
194
194
  get_chapter=lambda: Chapter(
195
195
  elements=[
196
- Text(
196
+ TextBlock(
197
197
  kind=TextKind.BODY,
198
198
  content=[
199
199
  "This is text with a footnote",
@@ -206,7 +206,7 @@ epub_data = EpubData(
206
206
  Footnote(
207
207
  id=1,
208
208
  contents=[
209
- Text(kind=TextKind.BODY, content=["This is the footnote content."]),
209
+ TextBlock(kind=TextKind.BODY, content=["This is the footnote content."]),
210
210
  ],
211
211
  ),
212
212
  ],
@@ -221,7 +221,7 @@ generate_epub(epub_data, "book_with_footnotes.epub")
221
221
  ### Add Tables
222
222
 
223
223
  ```python
224
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Table
224
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, Table
225
225
 
226
226
  epub_data = EpubData(
227
227
  chapters=[
@@ -229,7 +229,7 @@ epub_data = EpubData(
229
229
  title="Chapter 1",
230
230
  get_chapter=lambda: Chapter(
231
231
  elements=[
232
- Text(kind=TextKind.BODY, content=["Here's a table:"]),
232
+ TextBlock(kind=TextKind.BODY, content=["Here's a table:"]),
233
233
  Table(
234
234
  html_content="""
235
235
  <table>
@@ -250,8 +250,10 @@ generate_epub(epub_data, "book_with_tables.epub")
250
250
 
251
251
  ### Add Math Formulas
252
252
 
253
+ Block-level formulas:
254
+
253
255
  ```python
254
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Formula, LaTeXRender
256
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, Formula, LaTeXRender
255
257
 
256
258
  epub_data = EpubData(
257
259
  chapters=[
@@ -259,8 +261,8 @@ epub_data = EpubData(
259
261
  title="Chapter 1",
260
262
  get_chapter=lambda: Chapter(
261
263
  elements=[
262
- Text(kind=TextKind.BODY, content=["Pythagorean theorem:"]),
263
- Formula(latex_expression="x^2 + y^2 = z^2"), # LaTeX expression
264
+ TextBlock(kind=TextKind.BODY, content=["Pythagorean theorem:"]),
265
+ Formula(latex_expression="x^2 + y^2 = z^2"), # Block-level formula
264
266
  ]
265
267
  ),
266
268
  ),
@@ -271,10 +273,73 @@ epub_data = EpubData(
271
273
  generate_epub(epub_data, "book_with_math.epub", latex_render=LaTeXRender.MATHML)
272
274
  ```
273
275
 
276
+ Inline formulas embedded in text:
277
+
278
+ ```python
279
+ epub_data = EpubData(
280
+ chapters=[
281
+ TocItem(
282
+ title="Chapter 1",
283
+ get_chapter=lambda: Chapter(
284
+ elements=[
285
+ TextBlock(
286
+ kind=TextKind.BODY,
287
+ content=[
288
+ "The Pythagorean theorem ",
289
+ Formula(latex_expression="a^2 + b^2 = c^2"), # Inline formula
290
+ " is fundamental. Einstein's equation ",
291
+ Formula(latex_expression="E = mc^2"),
292
+ " shows mass-energy equivalence.",
293
+ ],
294
+ ),
295
+ ]
296
+ ),
297
+ ),
298
+ ],
299
+ )
300
+
301
+ generate_epub(epub_data, "book_with_inline_math.epub", latex_render=LaTeXRender.MATHML)
302
+ ```
303
+
304
+ ### Add Custom HTML Tags
305
+
306
+ You can embed custom HTML tags within text content:
307
+
308
+ ```python
309
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, HTMLTag
310
+
311
+ epub_data = EpubData(
312
+ chapters=[
313
+ TocItem(
314
+ title="Chapter 1",
315
+ get_chapter=lambda: Chapter(elements=[TextBlock(
316
+ kind=TextKind.BODY,
317
+ content=[
318
+ "This is normal text with ",
319
+ HTMLTag(
320
+ name="span",
321
+ attributes=[("class", "highlight"), ("style", "color: red;")],
322
+ content=["highlighted content"],
323
+ ),
324
+ " and more text with ",
325
+ HTMLTag(
326
+ name="strong",
327
+ content=["bold text"],
328
+ ),
329
+ ".",
330
+ ],
331
+ )]),
332
+ ),
333
+ ],
334
+ )
335
+
336
+ generate_epub(epub_data, "book_with_html_tags.epub")
337
+ ```
338
+
274
339
  ### Add Prefaces
275
340
 
276
341
  ```python
277
- from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, Text, TextKind
342
+ from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, TextBlock, TextKind
278
343
 
279
344
  epub_data = EpubData(
280
345
  meta=BookMeta(title="Book with Prefaces"),
@@ -283,8 +348,8 @@ epub_data = EpubData(
283
348
  title="Preface",
284
349
  get_chapter=lambda: Chapter(
285
350
  elements=[
286
- Text(kind=TextKind.HEADLINE, content=["Preface"]),
287
- Text(kind=TextKind.BODY, content=["This is the preface content..."]),
351
+ TextBlock(kind=TextKind.HEADLINE, content=["Preface"]),
352
+ TextBlock(kind=TextKind.BODY, content=["This is the preface content..."]),
288
353
  ]
289
354
  ),
290
355
  ),
@@ -294,7 +359,7 @@ epub_data = EpubData(
294
359
  title="Chapter 1",
295
360
  get_chapter=lambda: Chapter(
296
361
  elements=[
297
- Text(kind=TextKind.BODY, content=["Main content..."]),
362
+ TextBlock(kind=TextKind.BODY, content=["Main content..."]),
298
363
  ]
299
364
  ),
300
365
  ),
@@ -387,12 +452,12 @@ class Chapter:
387
452
 
388
453
  `ContentBlock` is a union of:
389
454
 
390
- - **`Text`**: Text paragraph
455
+ - **`TextBlock`**: Text paragraph
391
456
  ```python
392
457
  @dataclass
393
- class Text:
394
- kind: TextKind # BODY | HEADLINE | QUOTE
395
- content: list[str | Mark] # Text content with optional marks
458
+ class TextBlock:
459
+ kind: TextKind # BODY | HEADLINE | QUOTE
460
+ content: list[str | Mark | Formula | HTMLTag] # Text with optional marks, inline formulas, and HTML tags
396
461
  ```
397
462
 
398
463
  - **`Image`**: Image reference
@@ -417,6 +482,15 @@ class Chapter:
417
482
  latex_expression: str # LaTeX expression
418
483
  ```
419
484
 
485
+ - **`HTMLTag`**: HTML tag
486
+ ```python
487
+ @dataclass
488
+ class HTMLTag:
489
+ name: str # Tag name (e.g., "span", "div")
490
+ attributes: list[tuple[str, str]] = [] # List of (attribute, value) pairs
491
+ content: list[str | Mark | Formula | HTMLTag] = [] # Inner HTML content
492
+ ```
493
+
420
494
  #### `Footnote`
421
495
 
422
496
  Footnote/citation.
@@ -21,7 +21,7 @@ pip install epub-generator
21
21
  ### Generate Your First Book in 5 Minutes
22
22
 
23
23
  ```python
24
- from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, Text, TextKind
24
+ from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, TextBlock, TextKind
25
25
 
26
26
  # Prepare book data
27
27
  epub_data = EpubData(
@@ -34,9 +34,9 @@ epub_data = EpubData(
34
34
  title="Chapter 1",
35
35
  get_chapter=lambda: Chapter(
36
36
  elements=[
37
- Text(kind=TextKind.HEADLINE, content=["Chapter 1"]),
38
- Text(kind=TextKind.BODY, content=["This is the first paragraph."]),
39
- Text(kind=TextKind.BODY, content=["This is the second paragraph."]),
37
+ TextBlock(kind=TextKind.HEADLINE, content=["Chapter 1"]),
38
+ TextBlock(kind=TextKind.BODY, content=["This is the first paragraph."]),
39
+ TextBlock(kind=TextKind.BODY, content=["This is the second paragraph."]),
40
40
  ]
41
41
  ),
42
42
  ),
@@ -53,9 +53,9 @@ That's it! You now have a valid EPUB 3.0 ebook file.
53
53
 
54
54
  - **Minimal API**: Just one function call `generate_epub()`
55
55
  - **EPUB 3.0**: Generates standards-compliant EPUB 3.0 format
56
- - **Rich Content**: Supports text, images, tables, math formulas, footnotes
56
+ - **Rich Content**: Supports text, images, tables, math formulas (block-level and inline), footnotes, custom HTML tags
57
57
  - **Flexible Structure**: Nested chapters, prefaces, cover images
58
- - **Math Support**: LaTeX to MathML conversion
58
+ - **Math Support**: LaTeX to MathML/SVG conversion with inline formula support
59
59
  - **Type Safe**: Full type annotations included
60
60
 
61
61
  ## Advanced Usage
@@ -65,7 +65,7 @@ That's it! You now have a valid EPUB 3.0 ebook file.
65
65
  ```python
66
66
  from datetime import datetime, timezone
67
67
  from pathlib import Path
68
- from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, Text, TextKind
68
+ from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, TextBlock, TextKind
69
69
 
70
70
  epub_data = EpubData(
71
71
  meta=BookMeta(
@@ -84,8 +84,8 @@ epub_data = EpubData(
84
84
  title="Chapter 1",
85
85
  get_chapter=lambda: Chapter(
86
86
  elements=[
87
- Text(kind=TextKind.HEADLINE, content=["Chapter 1"]),
88
- Text(kind=TextKind.BODY, content=["Main content..."]),
87
+ TextBlock(kind=TextKind.HEADLINE, content=["Chapter 1"]),
88
+ TextBlock(kind=TextKind.BODY, content=["Main content..."]),
89
89
  ]
90
90
  ),
91
91
  ),
@@ -98,7 +98,7 @@ generate_epub(epub_data, "book_with_cover.epub")
98
98
  ### Nested Chapter Structure
99
99
 
100
100
  ```python
101
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind
101
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind
102
102
 
103
103
  epub_data = EpubData(
104
104
  chapters=[
@@ -109,7 +109,7 @@ epub_data = EpubData(
109
109
  title="Chapter 1.1",
110
110
  get_chapter=lambda: Chapter(
111
111
  elements=[
112
- Text(kind=TextKind.BODY, content=["Content 1.1..."]),
112
+ TextBlock(kind=TextKind.BODY, content=["Content 1.1..."]),
113
113
  ]
114
114
  ),
115
115
  ),
@@ -117,7 +117,7 @@ epub_data = EpubData(
117
117
  title="Chapter 1.2",
118
118
  get_chapter=lambda: Chapter(
119
119
  elements=[
120
- Text(kind=TextKind.BODY, content=["Content 1.2..."]),
120
+ TextBlock(kind=TextKind.BODY, content=["Content 1.2..."]),
121
121
  ]
122
122
  ),
123
123
  ),
@@ -133,7 +133,7 @@ generate_epub(epub_data, "book_with_nested_chapters.epub")
133
133
 
134
134
  ```python
135
135
  from pathlib import Path
136
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Image
136
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, Image
137
137
 
138
138
  epub_data = EpubData(
139
139
  chapters=[
@@ -141,7 +141,7 @@ epub_data = EpubData(
141
141
  title="Chapter 1",
142
142
  get_chapter=lambda: Chapter(
143
143
  elements=[
144
- Text(kind=TextKind.BODY, content=["Here's an image:"]),
144
+ TextBlock(kind=TextKind.BODY, content=["Here's an image:"]),
145
145
  Image(
146
146
  path=Path("image.png"), # Image path
147
147
  alt_text="Image description",
@@ -158,7 +158,7 @@ generate_epub(epub_data, "book_with_images.epub")
158
158
  ### Add Footnotes
159
159
 
160
160
  ```python
161
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Mark, Footnote
161
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, Mark, Footnote
162
162
 
163
163
  epub_data = EpubData(
164
164
  chapters=[
@@ -166,7 +166,7 @@ epub_data = EpubData(
166
166
  title="Chapter 1",
167
167
  get_chapter=lambda: Chapter(
168
168
  elements=[
169
- Text(
169
+ TextBlock(
170
170
  kind=TextKind.BODY,
171
171
  content=[
172
172
  "This is text with a footnote",
@@ -179,7 +179,7 @@ epub_data = EpubData(
179
179
  Footnote(
180
180
  id=1,
181
181
  contents=[
182
- Text(kind=TextKind.BODY, content=["This is the footnote content."]),
182
+ TextBlock(kind=TextKind.BODY, content=["This is the footnote content."]),
183
183
  ],
184
184
  ),
185
185
  ],
@@ -194,7 +194,7 @@ generate_epub(epub_data, "book_with_footnotes.epub")
194
194
  ### Add Tables
195
195
 
196
196
  ```python
197
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Table
197
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, Table
198
198
 
199
199
  epub_data = EpubData(
200
200
  chapters=[
@@ -202,7 +202,7 @@ epub_data = EpubData(
202
202
  title="Chapter 1",
203
203
  get_chapter=lambda: Chapter(
204
204
  elements=[
205
- Text(kind=TextKind.BODY, content=["Here's a table:"]),
205
+ TextBlock(kind=TextKind.BODY, content=["Here's a table:"]),
206
206
  Table(
207
207
  html_content="""
208
208
  <table>
@@ -223,8 +223,10 @@ generate_epub(epub_data, "book_with_tables.epub")
223
223
 
224
224
  ### Add Math Formulas
225
225
 
226
+ Block-level formulas:
227
+
226
228
  ```python
227
- from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Formula, LaTeXRender
229
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, Formula, LaTeXRender
228
230
 
229
231
  epub_data = EpubData(
230
232
  chapters=[
@@ -232,8 +234,8 @@ epub_data = EpubData(
232
234
  title="Chapter 1",
233
235
  get_chapter=lambda: Chapter(
234
236
  elements=[
235
- Text(kind=TextKind.BODY, content=["Pythagorean theorem:"]),
236
- Formula(latex_expression="x^2 + y^2 = z^2"), # LaTeX expression
237
+ TextBlock(kind=TextKind.BODY, content=["Pythagorean theorem:"]),
238
+ Formula(latex_expression="x^2 + y^2 = z^2"), # Block-level formula
237
239
  ]
238
240
  ),
239
241
  ),
@@ -244,10 +246,73 @@ epub_data = EpubData(
244
246
  generate_epub(epub_data, "book_with_math.epub", latex_render=LaTeXRender.MATHML)
245
247
  ```
246
248
 
249
+ Inline formulas embedded in text:
250
+
251
+ ```python
252
+ epub_data = EpubData(
253
+ chapters=[
254
+ TocItem(
255
+ title="Chapter 1",
256
+ get_chapter=lambda: Chapter(
257
+ elements=[
258
+ TextBlock(
259
+ kind=TextKind.BODY,
260
+ content=[
261
+ "The Pythagorean theorem ",
262
+ Formula(latex_expression="a^2 + b^2 = c^2"), # Inline formula
263
+ " is fundamental. Einstein's equation ",
264
+ Formula(latex_expression="E = mc^2"),
265
+ " shows mass-energy equivalence.",
266
+ ],
267
+ ),
268
+ ]
269
+ ),
270
+ ),
271
+ ],
272
+ )
273
+
274
+ generate_epub(epub_data, "book_with_inline_math.epub", latex_render=LaTeXRender.MATHML)
275
+ ```
276
+
277
+ ### Add Custom HTML Tags
278
+
279
+ You can embed custom HTML tags within text content:
280
+
281
+ ```python
282
+ from epub_generator import generate_epub, EpubData, TocItem, Chapter, TextBlock, TextKind, HTMLTag
283
+
284
+ epub_data = EpubData(
285
+ chapters=[
286
+ TocItem(
287
+ title="Chapter 1",
288
+ get_chapter=lambda: Chapter(elements=[TextBlock(
289
+ kind=TextKind.BODY,
290
+ content=[
291
+ "This is normal text with ",
292
+ HTMLTag(
293
+ name="span",
294
+ attributes=[("class", "highlight"), ("style", "color: red;")],
295
+ content=["highlighted content"],
296
+ ),
297
+ " and more text with ",
298
+ HTMLTag(
299
+ name="strong",
300
+ content=["bold text"],
301
+ ),
302
+ ".",
303
+ ],
304
+ )]),
305
+ ),
306
+ ],
307
+ )
308
+
309
+ generate_epub(epub_data, "book_with_html_tags.epub")
310
+ ```
311
+
247
312
  ### Add Prefaces
248
313
 
249
314
  ```python
250
- from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, Text, TextKind
315
+ from epub_generator import generate_epub, EpubData, BookMeta, TocItem, Chapter, TextBlock, TextKind
251
316
 
252
317
  epub_data = EpubData(
253
318
  meta=BookMeta(title="Book with Prefaces"),
@@ -256,8 +321,8 @@ epub_data = EpubData(
256
321
  title="Preface",
257
322
  get_chapter=lambda: Chapter(
258
323
  elements=[
259
- Text(kind=TextKind.HEADLINE, content=["Preface"]),
260
- Text(kind=TextKind.BODY, content=["This is the preface content..."]),
324
+ TextBlock(kind=TextKind.HEADLINE, content=["Preface"]),
325
+ TextBlock(kind=TextKind.BODY, content=["This is the preface content..."]),
261
326
  ]
262
327
  ),
263
328
  ),
@@ -267,7 +332,7 @@ epub_data = EpubData(
267
332
  title="Chapter 1",
268
333
  get_chapter=lambda: Chapter(
269
334
  elements=[
270
- Text(kind=TextKind.BODY, content=["Main content..."]),
335
+ TextBlock(kind=TextKind.BODY, content=["Main content..."]),
271
336
  ]
272
337
  ),
273
338
  ),
@@ -360,12 +425,12 @@ class Chapter:
360
425
 
361
426
  `ContentBlock` is a union of:
362
427
 
363
- - **`Text`**: Text paragraph
428
+ - **`TextBlock`**: Text paragraph
364
429
  ```python
365
430
  @dataclass
366
- class Text:
367
- kind: TextKind # BODY | HEADLINE | QUOTE
368
- content: list[str | Mark] # Text content with optional marks
431
+ class TextBlock:
432
+ kind: TextKind # BODY | HEADLINE | QUOTE
433
+ content: list[str | Mark | Formula | HTMLTag] # Text with optional marks, inline formulas, and HTML tags
369
434
  ```
370
435
 
371
436
  - **`Image`**: Image reference
@@ -390,6 +455,15 @@ class Chapter:
390
455
  latex_expression: str # LaTeX expression
391
456
  ```
392
457
 
458
+ - **`HTMLTag`**: HTML tag
459
+ ```python
460
+ @dataclass
461
+ class HTMLTag:
462
+ name: str # Tag name (e.g., "span", "div")
463
+ attributes: list[tuple[str, str]] = [] # List of (attribute, value) pairs
464
+ content: list[str | Mark | Formula | HTMLTag] = [] # Inner HTML content
465
+ ```
466
+
393
467
  #### `Footnote`
394
468
 
395
469
  Footnote/citation.
@@ -8,10 +8,11 @@ from .types import (
8
8
  EpubData,
9
9
  Footnote,
10
10
  Formula,
11
+ HTMLTag,
11
12
  Image,
12
13
  Mark,
13
14
  Table,
14
- Text,
15
+ TextBlock,
15
16
  TextKind,
16
17
  TocItem,
17
18
  )
@@ -29,10 +30,11 @@ __all__ = [
29
30
  "Chapter",
30
31
  "ChapterGetter",
31
32
  "ContentBlock",
32
- "Text",
33
+ "TextBlock",
33
34
  "TextKind",
34
35
  "Table",
35
36
  "Formula",
37
+ "HTMLTag",
36
38
  "Image",
37
39
  "Footnote",
38
40
  "Mark",
@@ -53,4 +53,16 @@ div.alt-wrapper th,
53
53
  div.alt-wrapper th {
54
54
  background-color: #f6f8fa;
55
55
  font-weight: 600;
56
+ }
57
+
58
+ span.formula-inline {
59
+ display: inline;
60
+ vertical-align: middle;
61
+ }
62
+
63
+ span.formula-inline img {
64
+ display: inline-block;
65
+ vertical-align: middle;
66
+ margin: 0 0.2em;
67
+ max-height: 1.2em;
56
68
  }
@@ -34,7 +34,12 @@ def process_table(context: Context, table: Table) -> Element | None:
34
34
  return None
35
35
 
36
36
 
37
- def process_formula(context: Context, formula: Formula) -> Element | None:
37
+ def process_formula(
38
+ context: Context,
39
+ formula: Formula,
40
+ inline_mode: bool,
41
+ ) -> Element | None:
42
+
38
43
  if context.latex_render == LaTeXRender.CLIPPING:
39
44
  return None
40
45
 
@@ -43,22 +48,28 @@ def process_formula(context: Context, formula: Formula) -> Element | None:
43
48
  return None
44
49
 
45
50
  if context.latex_render == LaTeXRender.MATHML:
46
- return _latex2mathml(latex_expr)
47
-
51
+ return _latex2mathml(
52
+ latex=latex_expr,
53
+ inline_mode=inline_mode,
54
+ )
48
55
  elif context.latex_render == LaTeXRender.SVG:
49
56
  svg_image = _latex_formula2svg(latex_expr)
50
57
  if svg_image is None:
51
58
  return None
52
59
  file_name = context.add_asset(
53
- data=svg_image,
54
- media_type="image/svg+xml",
60
+ data=svg_image,
61
+ media_type="image/svg+xml",
55
62
  file_ext=".svg",
56
63
  )
57
64
  img_element = Element("img")
58
65
  img_element.set("src", f"../assets/{file_name}")
59
66
  img_element.set("alt", "formula")
60
67
 
61
- wrapper = Element("div", attrib={"class": "alt-wrapper"})
68
+ if inline_mode:
69
+ wrapper = Element("span", attrib={"class": "formula-inline"})
70
+ else:
71
+ wrapper = Element("div", attrib={"class": "alt-wrapper"})
72
+
62
73
  wrapper.append(img_element)
63
74
  return wrapper
64
75
 
@@ -83,9 +94,12 @@ def process_image(context: Context, image: Image) -> Element | None:
83
94
  _ESCAPE_UNICODE_PATTERN = re.compile(r"&#x([0-9A-Fa-f]{5});")
84
95
 
85
96
 
86
- def _latex2mathml(latex: str) -> None | Element:
97
+ def _latex2mathml(latex: str, inline_mode: bool) -> None | Element:
87
98
  try:
88
- html_latex = convert(latex)
99
+ html_latex = convert(
100
+ latex=latex,
101
+ display="inline" if inline_mode else "block",
102
+ )
89
103
  except Exception:
90
104
  return None
91
105
 
@@ -7,14 +7,15 @@ from ..types import (
7
7
  Chapter,
8
8
  ContentBlock,
9
9
  Formula,
10
+ HTMLTag,
10
11
  Image,
11
12
  Mark,
12
13
  Table,
13
- Text,
14
+ TextBlock,
14
15
  TextKind,
15
16
  )
16
- from .xml_utils import serialize_element, set_epub_type
17
17
  from .gen_asset import process_formula, process_image, process_table
18
+ from .xml_utils import serialize_element, set_epub_type
18
19
 
19
20
 
20
21
  def generate_chapter(
@@ -88,7 +89,7 @@ def _render_footnotes(
88
89
 
89
90
 
90
91
  def _render_content_block(context: Context, block: ContentBlock) -> Element | None:
91
- if isinstance(block, Text):
92
+ if isinstance(block, TextBlock):
92
93
  if block.kind == TextKind.HEADLINE:
93
94
  container = Element("h1")
94
95
  elif block.kind == TextKind.QUOTE:
@@ -98,8 +99,11 @@ def _render_content_block(context: Context, block: ContentBlock) -> Element | No
98
99
  else:
99
100
  raise ValueError(f"Unknown TextKind: {block.kind}")
100
101
 
101
- _render_text_content(container, block.content)
102
-
102
+ _render_text_content(
103
+ context=context,
104
+ parent=container,
105
+ content=block.content,
106
+ )
103
107
  if block.kind == TextKind.QUOTE:
104
108
  blockquote = Element("blockquote")
105
109
  blockquote.append(container)
@@ -111,7 +115,7 @@ def _render_content_block(context: Context, block: ContentBlock) -> Element | No
111
115
  return process_table(context, block)
112
116
 
113
117
  elif isinstance(block, Formula):
114
- return process_formula(context, block)
118
+ return process_formula(context, block, inline_mode=False)
115
119
 
116
120
  elif isinstance(block, Image):
117
121
  return process_image(context, block)
@@ -120,7 +124,7 @@ def _render_content_block(context: Context, block: ContentBlock) -> Element | No
120
124
  return None
121
125
 
122
126
 
123
- def _render_text_content(parent: Element, content: list[str | Mark]) -> None:
127
+ def _render_text_content(context: Context, parent: Element, content: list[str | Mark | Formula | HTMLTag]) -> None:
124
128
  """Render text content with inline citation marks."""
125
129
  current_element = parent
126
130
  for item in content:
@@ -135,6 +139,29 @@ def _render_text_content(parent: Element, content: list[str | Mark]) -> None:
135
139
  current_element.tail = item
136
140
  else:
137
141
  current_element.tail += item
142
+
143
+ elif isinstance(item, HTMLTag):
144
+ tag_element = Element(item.name)
145
+ for attr, value in item.attributes:
146
+ tag_element.set(attr, value)
147
+ _render_text_content(
148
+ context=context,
149
+ parent=tag_element,
150
+ content=item.content,
151
+ )
152
+ parent.append(tag_element)
153
+ current_element = tag_element
154
+
155
+ elif isinstance(item, Formula):
156
+ formula_element = process_formula(
157
+ context=context,
158
+ formula=item,
159
+ inline_mode=True,
160
+ )
161
+ if formula_element is not None:
162
+ parent.append(formula_element)
163
+ current_element = formula_element
164
+
138
165
  elif isinstance(item, Mark):
139
166
  # EPUB 3.0 noteref with semantic attributes
140
167
  anchor = Element("a")
@@ -6,9 +6,10 @@ from uuid import uuid4
6
6
  from zipfile import ZipFile
7
7
 
8
8
  from ..context import Context, Template
9
+ from ..html_tag import search_content
9
10
  from ..i18n import I18N
10
11
  from ..options import LaTeXRender, TableRender
11
- from ..types import EpubData, Formula
12
+ from ..types import Chapter, EpubData, Formula, TextBlock
12
13
  from .gen_chapter import generate_chapter
13
14
  from .gen_nav import gen_nav
14
15
  from .gen_toc import NavPoint, gen_toc
@@ -135,10 +136,23 @@ def _write_chapters_from_data(
135
136
  assert_not_aborted()
136
137
 
137
138
 
138
- def _chapter_has_formula(chapter) -> bool:
139
+ def _chapter_has_formula(chapter: Chapter) -> bool:
140
+ """Check if chapter contains any formulas (block-level or inline)."""
139
141
  for element in chapter.elements:
140
142
  if isinstance(element, Formula):
141
143
  return True
144
+ if isinstance(element, TextBlock):
145
+ for item in search_content(element.content):
146
+ if isinstance(item, Formula):
147
+ return True
148
+ for footnote in chapter.footnotes:
149
+ for content_block in footnote.contents:
150
+ if isinstance(content_block, Formula):
151
+ return True
152
+ if isinstance(content_block, TextBlock):
153
+ for item in search_content(content_block.content):
154
+ if isinstance(item, Formula):
155
+ return True
142
156
  return False
143
157
 
144
158
  def _write_basic_files(
@@ -0,0 +1,31 @@
1
+ import re
2
+ from xml.etree.ElementTree import Element, tostring
3
+
4
+ _EPUB_NS = "http://www.idpf.org/2007/ops"
5
+ _MATHML_NS = "http://www.w3.org/1998/Math/MathML"
6
+
7
+
8
+ def set_epub_type(element: Element, epub_type: str) -> None:
9
+ element.set(f"{{{_EPUB_NS}}}type", epub_type)
10
+
11
+ def serialize_element(element: Element) -> str:
12
+ xml_string = tostring(element, encoding="unicode")
13
+ for prefix, namespace_uri, keep_xmlns in (
14
+ ("epub", _EPUB_NS, False), # EPUB namespace: remove xmlns (declared at root)
15
+ ("m", _MATHML_NS, True), # MathML namespace: keep xmlns with clean prefix
16
+ ):
17
+ xml_string = xml_string.replace(f"{{{namespace_uri}}}", f"{prefix}:")
18
+ pattern = r"xmlns:(ns\d+)=\"" + re.escape(namespace_uri) + r"\""
19
+ matches = re.findall(pattern, xml_string)
20
+
21
+ for ns_prefix in matches:
22
+ if keep_xmlns:
23
+ xml_string = xml_string.replace(
24
+ f" xmlns:{ns_prefix}=\"{namespace_uri}\"",
25
+ f" xmlns:{prefix}=\"{namespace_uri}\""
26
+ )
27
+ else:
28
+ xml_string = xml_string.replace(f" xmlns:{ns_prefix}=\"{namespace_uri}\"", "")
29
+ xml_string = xml_string.replace(f"{ns_prefix}:", f"{prefix}:")
30
+
31
+ return xml_string
@@ -0,0 +1,11 @@
1
+ from typing import Generator
2
+
3
+ from .types import Formula, HTMLTag, Mark
4
+
5
+
6
+ def search_content(content: list[str | Mark | Formula | HTMLTag]) -> Generator[str | Mark | Formula, None, None]:
7
+ for child in content:
8
+ if isinstance(child, HTMLTag):
9
+ yield from search_content(child.content)
10
+ else:
11
+ yield child
@@ -83,13 +83,6 @@ class Mark:
83
83
  id: int
84
84
  """Citation ID, matches Footnote.id"""
85
85
 
86
- @dataclass
87
- class Text:
88
- kind: TextKind
89
- """Kind of text block."""
90
- content: list[str | Mark]
91
- """Text content with optional citation marks."""
92
-
93
86
  @dataclass
94
87
  class Table:
95
88
  """HTML table."""
@@ -113,6 +106,12 @@ class Image:
113
106
  alt_text: str = "image"
114
107
  """Alt text (defaults to "image")"""
115
108
 
109
+ @dataclass
110
+ class TextBlock:
111
+ kind: TextKind
112
+ """Kind of text block."""
113
+ content: list["str | Mark | Formula | HTMLTag"]
114
+ """Text content with optional citation marks."""
116
115
 
117
116
  @dataclass
118
117
  class Footnote:
@@ -127,7 +126,7 @@ class Footnote:
127
126
  """Content blocks"""
128
127
 
129
128
 
130
- ContentBlock = Text | Table | Formula | Image
129
+ ContentBlock = TextBlock | Table | Formula | Image
131
130
  """Union of all content blocks that appear in main chapter content."""
132
131
 
133
132
  @dataclass
@@ -139,4 +138,17 @@ class Chapter:
139
138
  footnotes: list[Footnote] = field(default_factory=list)
140
139
  """Footnotes"""
141
140
 
142
- ChapterGetter = Callable[[], Chapter]
141
+ ChapterGetter = Callable[[], Chapter]
142
+
143
+ @dataclass
144
+ class HTMLTag:
145
+ """Generic HTML tag representation."""
146
+
147
+ name: str
148
+ """Tag name"""
149
+
150
+ attributes: list[tuple[str, str]] = field(default_factory=list)
151
+ """List of (attribute, value) pairs"""
152
+
153
+ content: list["str | Mark | Formula | HTMLTag"] = field(default_factory=list)
154
+ """Inner HTML content"""
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "epub-generator"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "A simple Python EPUB 3.0 generator with a single API call"
5
5
  authors = ["Tao Zeyu <i@taozeyu.com>"]
6
6
  license = "MIT"
@@ -1,18 +0,0 @@
1
- import re
2
- from xml.etree.ElementTree import Element, tostring
3
-
4
- _EPUB_NS = "http://www.idpf.org/2007/ops"
5
-
6
-
7
- def set_epub_type(element: Element, epub_type: str) -> None:
8
- element.set(f"{{{_EPUB_NS}}}type", epub_type)
9
-
10
- def serialize_element(element: Element) -> str:
11
- xml_string = tostring(element, encoding="unicode")
12
- xml_string = xml_string.replace(f"{{{_EPUB_NS}}}", "epub:")
13
- ns_pattern = r'xmlns:(ns\d+)="' + re.escape(_EPUB_NS) + r'"'
14
- matches = re.findall(ns_pattern, xml_string)
15
- for ns_prefix in matches:
16
- xml_string = xml_string.replace(f' xmlns:{ns_prefix}="{_EPUB_NS}"', "")
17
- xml_string = xml_string.replace(f"{ns_prefix}:", "epub:")
18
- return xml_string
File without changes