epub-generator 0.1.1__tar.gz → 0.1.2__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 (25) hide show
  1. {epub_generator-0.1.1 → epub_generator-0.1.2}/PKG-INFO +35 -5
  2. {epub_generator-0.1.1 → epub_generator-0.1.2}/README.md +34 -4
  3. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/data/style.css.jinja +12 -0
  4. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/generation/gen_asset.py +22 -8
  5. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/generation/gen_chapter.py +15 -4
  6. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/generation/gen_epub.py +14 -1
  7. epub_generator-0.1.2/epub_generator/generation/xml_utils.py +31 -0
  8. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/types.py +6 -7
  9. {epub_generator-0.1.1 → epub_generator-0.1.2}/pyproject.toml +1 -1
  10. epub_generator-0.1.1/epub_generator/generation/xml_utils.py +0 -18
  11. {epub_generator-0.1.1 → epub_generator-0.1.2}/LICENSE +0 -0
  12. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/__init__.py +0 -0
  13. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/context.py +0 -0
  14. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/data/container.xml.jinja +0 -0
  15. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/data/content.opf.jinja +0 -0
  16. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/data/cover.xhtml.jinja +0 -0
  17. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/data/mimetype.jinja +0 -0
  18. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/data/nav.xhtml.jinja +0 -0
  19. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/data/part.xhtml.jinja +0 -0
  20. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/generation/__init__.py +0 -0
  21. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/generation/gen_nav.py +0 -0
  22. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/generation/gen_toc.py +0 -0
  23. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/i18n.py +0 -0
  24. {epub_generator-0.1.1 → epub_generator-0.1.2}/epub_generator/options.py +0 -0
  25. {epub_generator-0.1.1 → epub_generator-0.1.2}/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.2
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
@@ -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
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
@@ -250,6 +250,8 @@ 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
256
  from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Formula, LaTeXRender
255
257
 
@@ -260,7 +262,7 @@ epub_data = EpubData(
260
262
  get_chapter=lambda: Chapter(
261
263
  elements=[
262
264
  Text(kind=TextKind.BODY, content=["Pythagorean theorem:"]),
263
- Formula(latex_expression="x^2 + y^2 = z^2"), # LaTeX expression
265
+ Formula(latex_expression="x^2 + y^2 = z^2"), # Block-level formula
264
266
  ]
265
267
  ),
266
268
  ),
@@ -271,6 +273,34 @@ 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
+ Text(
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
+
274
304
  ### Add Prefaces
275
305
 
276
306
  ```python
@@ -392,7 +422,7 @@ class Chapter:
392
422
  @dataclass
393
423
  class Text:
394
424
  kind: TextKind # BODY | HEADLINE | QUOTE
395
- content: list[str | Mark] # Text content with optional marks
425
+ content: list[str | Mark | Formula] # Text with optional marks and inline formulas
396
426
  ```
397
427
 
398
428
  - **`Image`**: Image reference
@@ -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
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
@@ -223,6 +223,8 @@ 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
229
  from epub_generator import generate_epub, EpubData, TocItem, Chapter, Text, TextKind, Formula, LaTeXRender
228
230
 
@@ -233,7 +235,7 @@ epub_data = EpubData(
233
235
  get_chapter=lambda: Chapter(
234
236
  elements=[
235
237
  Text(kind=TextKind.BODY, content=["Pythagorean theorem:"]),
236
- Formula(latex_expression="x^2 + y^2 = z^2"), # LaTeX expression
238
+ Formula(latex_expression="x^2 + y^2 = z^2"), # Block-level formula
237
239
  ]
238
240
  ),
239
241
  ),
@@ -244,6 +246,34 @@ 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
+ Text(
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
+
247
277
  ### Add Prefaces
248
278
 
249
279
  ```python
@@ -365,7 +395,7 @@ class Chapter:
365
395
  @dataclass
366
396
  class Text:
367
397
  kind: TextKind # BODY | HEADLINE | QUOTE
368
- content: list[str | Mark] # Text content with optional marks
398
+ content: list[str | Mark | Formula] # Text with optional marks and inline formulas
369
399
  ```
370
400
 
371
401
  - **`Image`**: Image reference
@@ -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
 
@@ -13,8 +13,8 @@ from ..types import (
13
13
  Text,
14
14
  TextKind,
15
15
  )
16
- from .xml_utils import serialize_element, set_epub_type
17
16
  from .gen_asset import process_formula, process_image, process_table
17
+ from .xml_utils import serialize_element, set_epub_type
18
18
 
19
19
 
20
20
  def generate_chapter(
@@ -98,7 +98,7 @@ def _render_content_block(context: Context, block: ContentBlock) -> Element | No
98
98
  else:
99
99
  raise ValueError(f"Unknown TextKind: {block.kind}")
100
100
 
101
- _render_text_content(container, block.content)
101
+ _render_text_content(context, container, block.content)
102
102
 
103
103
  if block.kind == TextKind.QUOTE:
104
104
  blockquote = Element("blockquote")
@@ -111,7 +111,7 @@ def _render_content_block(context: Context, block: ContentBlock) -> Element | No
111
111
  return process_table(context, block)
112
112
 
113
113
  elif isinstance(block, Formula):
114
- return process_formula(context, block)
114
+ return process_formula(context, block, inline_mode=False)
115
115
 
116
116
  elif isinstance(block, Image):
117
117
  return process_image(context, block)
@@ -120,7 +120,7 @@ def _render_content_block(context: Context, block: ContentBlock) -> Element | No
120
120
  return None
121
121
 
122
122
 
123
- def _render_text_content(parent: Element, content: list[str | Mark]) -> None:
123
+ def _render_text_content(context: Context, parent: Element, content: list[str | Mark | Formula]) -> None:
124
124
  """Render text content with inline citation marks."""
125
125
  current_element = parent
126
126
  for item in content:
@@ -135,6 +135,17 @@ def _render_text_content(parent: Element, content: list[str | Mark]) -> None:
135
135
  current_element.tail = item
136
136
  else:
137
137
  current_element.tail += item
138
+
139
+ elif isinstance(item, Formula):
140
+ formula_element = process_formula(
141
+ context=context,
142
+ formula=item,
143
+ inline_mode=True,
144
+ )
145
+ if formula_element is not None:
146
+ parent.append(formula_element)
147
+ current_element = formula_element
148
+
138
149
  elif isinstance(item, Mark):
139
150
  # EPUB 3.0 noteref with semantic attributes
140
151
  anchor = Element("a")
@@ -8,7 +8,7 @@ from zipfile import ZipFile
8
8
  from ..context import Context, Template
9
9
  from ..i18n import I18N
10
10
  from ..options import LaTeXRender, TableRender
11
- from ..types import EpubData, Formula
11
+ from ..types import EpubData, Formula, Text
12
12
  from .gen_chapter import generate_chapter
13
13
  from .gen_nav import gen_nav
14
14
  from .gen_toc import NavPoint, gen_toc
@@ -136,9 +136,22 @@ def _write_chapters_from_data(
136
136
 
137
137
 
138
138
  def _chapter_has_formula(chapter) -> bool:
139
+ """Check if chapter contains any formulas (block-level or inline)."""
139
140
  for element in chapter.elements:
140
141
  if isinstance(element, Formula):
141
142
  return True
143
+ if isinstance(element, Text):
144
+ for item in element.content:
145
+ if isinstance(item, Formula):
146
+ return True
147
+ for footnote in chapter.footnotes:
148
+ for content_block in footnote.contents:
149
+ if isinstance(content_block, Formula):
150
+ return True
151
+ if isinstance(content_block, Text):
152
+ for item in content_block.content:
153
+ if isinstance(item, Formula):
154
+ return True
142
155
  return False
143
156
 
144
157
  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
@@ -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 Text:
111
+ kind: TextKind
112
+ """Kind of text block."""
113
+ content: list[str | Mark | Formula]
114
+ """Text content with optional citation marks."""
116
115
 
117
116
  @dataclass
118
117
  class Footnote:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "epub-generator"
3
- version = "0.1.1"
3
+ version = "0.1.2"
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