bookcraft 0.1.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.
- bookcraft-0.1.0/PKG-INFO +66 -0
- bookcraft-0.1.0/README.md +49 -0
- bookcraft-0.1.0/pyproject.toml +20 -0
- bookcraft-0.1.0/src/bookcraft/__init__.py +3 -0
- bookcraft-0.1.0/src/bookcraft/_generate.py +48 -0
- bookcraft-0.1.0/src/bookcraft/book.py +188 -0
- bookcraft-0.1.0/src/bookcraft/cell/CellFactory.py +56 -0
- bookcraft-0.1.0/src/bookcraft/cell/__init__.py +0 -0
- bookcraft-0.1.0/src/bookcraft/cell/rules/BreakLineRule.py +14 -0
- bookcraft-0.1.0/src/bookcraft/cell/rules/CharRule.py +17 -0
- bookcraft-0.1.0/src/bookcraft/cell/rules/HeaderSpacingRule.py +14 -0
- bookcraft-0.1.0/src/bookcraft/cell/rules/Rule.py +23 -0
- bookcraft-0.1.0/src/bookcraft/cell/rules/__init__.py +0 -0
- bookcraft-0.1.0/src/bookcraft/cli.py +23 -0
- bookcraft-0.1.0/src/bookcraft/config.py +134 -0
- bookcraft-0.1.0/src/bookcraft/cursor/CursorModifierFactory.py +99 -0
- bookcraft-0.1.0/src/bookcraft/cursor/CursorModifierProcessor.py +40 -0
- bookcraft-0.1.0/src/bookcraft/cursor/CursorModifierReducer.py +28 -0
- bookcraft-0.1.0/src/bookcraft/cursor/__init__.py +0 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/BlankSpaceRule.py +20 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/ContentsItemRule.py +15 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/DarkenRoleRule.py +23 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/DefaultRule.py +11 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/HeaderRule.py +12 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/HyphenRule.py +11 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/ItalicRule.py +25 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/KeywordRule.py +25 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/ParenthesisRule.py +17 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/RoleRule.py +23 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/Rule.py +23 -0
- bookcraft-0.1.0/src/bookcraft/cursor/rules/__init__.py +0 -0
- bookcraft-0.1.0/src/bookcraft/helpers.py +110 -0
- bookcraft-0.1.0/src/bookcraft/models.py +83 -0
- bookcraft-0.1.0/src/bookcraft/specs/AfterRoleSpecification.py +16 -0
- bookcraft-0.1.0/src/bookcraft/specs/FirstLineSpecification.py +7 -0
- bookcraft-0.1.0/src/bookcraft/specs/IsBoldSpecification.py +7 -0
- bookcraft-0.1.0/src/bookcraft/specs/IsCharEqualSpecification.py +13 -0
- bookcraft-0.1.0/src/bookcraft/specs/IsEndOfLineSpecification.py +11 -0
- bookcraft-0.1.0/src/bookcraft/specs/IsItalicSpecification.py +7 -0
- bookcraft-0.1.0/src/bookcraft/specs/LineHasCharSpecification.py +17 -0
- bookcraft-0.1.0/src/bookcraft/specs/NextCharEquals.py +19 -0
- bookcraft-0.1.0/src/bookcraft/specs/PreviousCharEquals.py +16 -0
- bookcraft-0.1.0/src/bookcraft/specs/Specification.py +50 -0
- bookcraft-0.1.0/src/bookcraft/specs/__init__.py +0 -0
bookcraft-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: bookcraft
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A PDF book generator for structured ritual and ceremonial texts
|
|
5
|
+
Author: Albert Kolozsvari
|
|
6
|
+
Author-email: albertartk@gmail.com
|
|
7
|
+
Requires-Python: >=3.11.0,<4.0.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Dist: PyYAML (>=6.0.2)
|
|
13
|
+
Requires-Dist: fpdf2 (>=2.8.5,<3.0.0)
|
|
14
|
+
Requires-Dist: typing-extensions (>=4.15.0,<5.0.0)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# bookcraft
|
|
18
|
+
|
|
19
|
+
A Python library for generating PDF books from structured text content.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install bookcraft
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from bookcraft import generate
|
|
31
|
+
|
|
32
|
+
generate(
|
|
33
|
+
books_path="./books/",
|
|
34
|
+
settings_path="./c-settings.yaml",
|
|
35
|
+
fonts_path="./fonts.yaml",
|
|
36
|
+
keywords_path="./c-keywords.yaml",
|
|
37
|
+
output_path="./output/craft.pdf",
|
|
38
|
+
mode="craft",
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Modes
|
|
43
|
+
|
|
44
|
+
| Mode | Description |
|
|
45
|
+
|------|-------------|
|
|
46
|
+
| `craft` | Craft, light theme |
|
|
47
|
+
| `craft-dark` | Craft, dark theme |
|
|
48
|
+
| `ra` | Royal Arch, light theme |
|
|
49
|
+
| `ra-dark` | Royal Arch, dark theme |
|
|
50
|
+
|
|
51
|
+
## Content layout
|
|
52
|
+
|
|
53
|
+
Your content project should provide:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
your-project/
|
|
57
|
+
├── books/ # directories of page-N.txt files
|
|
58
|
+
├── fonts/ # font files referenced in fonts.yaml
|
|
59
|
+
├── fonts.yaml
|
|
60
|
+
├── c-settings.yaml
|
|
61
|
+
├── c-keywords.yaml
|
|
62
|
+
├── ra-settings.yaml
|
|
63
|
+
├── ra-keywords.yaml
|
|
64
|
+
└── output/ # generated PDFs written here
|
|
65
|
+
```
|
|
66
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# bookcraft
|
|
2
|
+
|
|
3
|
+
A Python library for generating PDF books from structured text content.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install bookcraft
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from bookcraft import generate
|
|
15
|
+
|
|
16
|
+
generate(
|
|
17
|
+
books_path="./books/",
|
|
18
|
+
settings_path="./c-settings.yaml",
|
|
19
|
+
fonts_path="./fonts.yaml",
|
|
20
|
+
keywords_path="./c-keywords.yaml",
|
|
21
|
+
output_path="./output/craft.pdf",
|
|
22
|
+
mode="craft",
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Modes
|
|
27
|
+
|
|
28
|
+
| Mode | Description |
|
|
29
|
+
|------|-------------|
|
|
30
|
+
| `craft` | Craft, light theme |
|
|
31
|
+
| `craft-dark` | Craft, dark theme |
|
|
32
|
+
| `ra` | Royal Arch, light theme |
|
|
33
|
+
| `ra-dark` | Royal Arch, dark theme |
|
|
34
|
+
|
|
35
|
+
## Content layout
|
|
36
|
+
|
|
37
|
+
Your content project should provide:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
your-project/
|
|
41
|
+
├── books/ # directories of page-N.txt files
|
|
42
|
+
├── fonts/ # font files referenced in fonts.yaml
|
|
43
|
+
├── fonts.yaml
|
|
44
|
+
├── c-settings.yaml
|
|
45
|
+
├── c-keywords.yaml
|
|
46
|
+
├── ra-settings.yaml
|
|
47
|
+
├── ra-keywords.yaml
|
|
48
|
+
└── output/ # generated PDFs written here
|
|
49
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "bookcraft"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A PDF book generator for structured ritual and ceremonial texts"
|
|
5
|
+
authors = ["Albert Kolozsvari <albertartk@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "bookcraft", from = "src"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.11.0"
|
|
11
|
+
PyYAML = ">=6.0.2"
|
|
12
|
+
fpdf2 = "^2.8.5"
|
|
13
|
+
typing-extensions = "^4.15.0"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.scripts]
|
|
16
|
+
bookcraft = "bookcraft.cli:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["poetry-core"]
|
|
20
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from bookcraft.book import Book
|
|
2
|
+
from bookcraft.cell.CellFactory import CellFactory
|
|
3
|
+
from bookcraft.config import load_config
|
|
4
|
+
from bookcraft.cursor.CursorModifierFactory import CursorModifierFactory
|
|
5
|
+
from bookcraft.cursor.CursorModifierProcessor import CursorModifierProcessor
|
|
6
|
+
from bookcraft.cursor.CursorModifierReducer import CursorModifierReducer
|
|
7
|
+
from bookcraft.helpers import get_files
|
|
8
|
+
|
|
9
|
+
_SWITCH_MAP = {
|
|
10
|
+
"craft": "c-",
|
|
11
|
+
"craft-dark": "c-dark-",
|
|
12
|
+
"ra": "ra-",
|
|
13
|
+
"ra-dark": "ra-dark-",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate(
|
|
18
|
+
books_path: str,
|
|
19
|
+
settings_path: str,
|
|
20
|
+
fonts_path: str,
|
|
21
|
+
keywords_path: str,
|
|
22
|
+
output_path: str,
|
|
23
|
+
mode: str = "craft",
|
|
24
|
+
) -> None:
|
|
25
|
+
if mode not in _SWITCH_MAP:
|
|
26
|
+
raise ValueError(f"Unknown mode '{mode}'. Choose from: {', '.join(_SWITCH_MAP)}")
|
|
27
|
+
|
|
28
|
+
switch = _SWITCH_MAP[mode]
|
|
29
|
+
config = load_config(books_path, settings_path, fonts_path, keywords_path, switch)
|
|
30
|
+
|
|
31
|
+
book = Book(config)
|
|
32
|
+
book.set_title(config.SETTINGS["title"]["text"])
|
|
33
|
+
book.set_book_font(config.FONTS)
|
|
34
|
+
|
|
35
|
+
for book_title in config.SETTINGS["books"]:
|
|
36
|
+
if config.is_dark:
|
|
37
|
+
book.page_background = (18, 18, 18)
|
|
38
|
+
|
|
39
|
+
book.set_path(books_path + book_title)
|
|
40
|
+
book.set_margin(config.PAGE)
|
|
41
|
+
book.set_subject(book_title)
|
|
42
|
+
book.set_cm_factory(CursorModifierFactory())
|
|
43
|
+
book.set_cell_factory(CellFactory())
|
|
44
|
+
book.set_cm_processor(CursorModifierProcessor())
|
|
45
|
+
book.set_cm_reducer(CursorModifierReducer())
|
|
46
|
+
book.set_pages(get_files(books_path + book_title))
|
|
47
|
+
|
|
48
|
+
book.build(output_path)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fpdf import FPDF
|
|
4
|
+
|
|
5
|
+
from bookcraft.cell.CellFactory import CellFactory
|
|
6
|
+
from bookcraft.config import Config
|
|
7
|
+
from bookcraft.cursor.CursorModifierFactory import CursorModifierFactory
|
|
8
|
+
from bookcraft.cursor.CursorModifierProcessor import CursorModifierProcessor
|
|
9
|
+
from bookcraft.cursor.CursorModifierReducer import CursorModifierReducer
|
|
10
|
+
from bookcraft.helpers import get_pages
|
|
11
|
+
from bookcraft.models import Cell, Context, CursorModifier, Page
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Book(FPDF):
|
|
15
|
+
def __init__(self, config: Config) -> Book:
|
|
16
|
+
self.config = config
|
|
17
|
+
format = (535, 785) if config.is_ra else (475, 785)
|
|
18
|
+
super().__init__(unit="pt", format=format)
|
|
19
|
+
self._page_subjects = {}
|
|
20
|
+
|
|
21
|
+
def header(self) -> None:
|
|
22
|
+
subject = self._page_subjects.get(self.page_no(), self.subject)
|
|
23
|
+
if "Cover" in subject and self.config.is_ra:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
page_no = self.page_no() + 22 if self.config.is_ra else self.page_no() - 5
|
|
27
|
+
self.set_font(**self.config.TEMPLATE_FONT)
|
|
28
|
+
self.set_text_color(*self.config.TEMPLATE_COLOR)
|
|
29
|
+
self.set_draw_color(*self.config.TEMPLATE_COLOR)
|
|
30
|
+
width = self.w - self.l_margin - self.r_margin
|
|
31
|
+
height = self.config.TEMPLATE_HEIGHT
|
|
32
|
+
page_no_width = self.get_string_width(f"{page_no}")
|
|
33
|
+
|
|
34
|
+
# Use per-page subject if available
|
|
35
|
+
subject = self._page_subjects.get(self.page_no(), self.subject)
|
|
36
|
+
subject_width = self.get_string_width(subject)
|
|
37
|
+
|
|
38
|
+
line_start = self.r_margin
|
|
39
|
+
line_end = self.w - self.r_margin
|
|
40
|
+
|
|
41
|
+
if page_no > 0 and page_no < 150:
|
|
42
|
+
self.cell(subject_width, height, subject)
|
|
43
|
+
self.cell(width - page_no_width - subject_width, height, "", 0)
|
|
44
|
+
self.cell(page_no_width, height, f"{page_no}", 0, 1)
|
|
45
|
+
self.cell(width, height, "", 0, 1)
|
|
46
|
+
self.dashed_line(line_start, self.y, line_end, self.y, 3, 3)
|
|
47
|
+
self.cell(width, height, "", 0, 1)
|
|
48
|
+
self.cell(width, height / 2, "", 0, 1)
|
|
49
|
+
|
|
50
|
+
def set_path(self, book_path: str) -> Book:
|
|
51
|
+
self.book_path = book_path
|
|
52
|
+
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def set_book_font(self, fonts: dict) -> Book:
|
|
56
|
+
[self.add_font(**font, uni=True) for font in fonts]
|
|
57
|
+
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def set_margin(self, page: dict) -> Book:
|
|
61
|
+
self.set_top_margin(page["top_margin"])
|
|
62
|
+
self.set_left_margin(page["margin_size"])
|
|
63
|
+
self.set_right_margin(page["margin_size"])
|
|
64
|
+
self.set_auto_page_break(True, page["bottom_margin"])
|
|
65
|
+
self.c_margin = 0
|
|
66
|
+
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def set_title(self, title: str) -> Book:
|
|
70
|
+
title = title.split("/")[-1]
|
|
71
|
+
super().set_title(title)
|
|
72
|
+
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def set_subject(self, subject: str) -> Book:
|
|
76
|
+
subject = subject.split("/")[-1]
|
|
77
|
+
super().set_subject(subject)
|
|
78
|
+
# Mark all future pages with this subject until changed
|
|
79
|
+
self._current_subject = subject
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def set_cm_reducer(self, cm_reducer: CursorModifierReducer) -> Book:
|
|
83
|
+
self.cm_reducer = cm_reducer
|
|
84
|
+
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def set_cm_processor(self, cm_processor: CursorModifierProcessor) -> Book:
|
|
88
|
+
self.cm_processor = cm_processor
|
|
89
|
+
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def set_cm_factory(self, cm_factory: CursorModifierFactory) -> Book:
|
|
93
|
+
self.cm_factory = cm_factory
|
|
94
|
+
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def set_cell_factory(self, cell_factory: CellFactory) -> Book:
|
|
98
|
+
self.cell_factory = cell_factory
|
|
99
|
+
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def set_pages(self, pages: list[Page]) -> Book:
|
|
103
|
+
self.modifiers: list[CursorModifier] = []
|
|
104
|
+
|
|
105
|
+
for page_index in range(1, len(pages) + 1):
|
|
106
|
+
memory = get_pages(self.book_path, page_index, 2)
|
|
107
|
+
self._print_page(memory)
|
|
108
|
+
# After adding a page, record the subject for that page number
|
|
109
|
+
self._page_subjects[self.page_no()] = getattr(self, "_current_subject", getattr(self, "subject", ""))
|
|
110
|
+
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def build(self, output_path: str) -> None:
|
|
114
|
+
self.output(output_path)
|
|
115
|
+
|
|
116
|
+
def _print_page(self, memory: list[Page]) -> list[CursorModifier]:
|
|
117
|
+
self.add_page()
|
|
118
|
+
|
|
119
|
+
page = memory[0]
|
|
120
|
+
for i in range(len(page)):
|
|
121
|
+
line = page[i]
|
|
122
|
+
body = []
|
|
123
|
+
|
|
124
|
+
for j in range(len(line)):
|
|
125
|
+
# get previous cell cursor
|
|
126
|
+
cursor = self.cm_reducer.reduce(self.modifiers)
|
|
127
|
+
context = Context(i, j, memory, self.config, cursor)
|
|
128
|
+
|
|
129
|
+
new_cms = self.cm_factory.resolve(context)
|
|
130
|
+
self.modifiers.extend(new_cms)
|
|
131
|
+
self.modifiers = self.cm_processor.process(self.modifiers)
|
|
132
|
+
|
|
133
|
+
# get current cell cursor
|
|
134
|
+
cursor = self.cm_reducer.reduce(self.modifiers)
|
|
135
|
+
context = Context(i, j, memory, self.config, cursor)
|
|
136
|
+
|
|
137
|
+
# get new cells
|
|
138
|
+
cells = self.cell_factory.create_cells(context)
|
|
139
|
+
|
|
140
|
+
if not cells:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
body.extend(cells)
|
|
144
|
+
|
|
145
|
+
self._print_line(body, line)
|
|
146
|
+
|
|
147
|
+
def _print_line(self, cells: list[Cell], memory_line: list[str]) -> None:
|
|
148
|
+
cells = self._justify_line(cells, memory_line)
|
|
149
|
+
for cell in cells:
|
|
150
|
+
cursor = cell.cursor
|
|
151
|
+
self.set_font(cursor.family, cursor.style, cursor.size)
|
|
152
|
+
self.set_text_color(*cursor.colour)
|
|
153
|
+
if cursor.fill is not None:
|
|
154
|
+
self.set_fill_color(*cursor.fill)
|
|
155
|
+
self.set_draw_color(*cursor.fill)
|
|
156
|
+
else:
|
|
157
|
+
cell.has_fill = False
|
|
158
|
+
|
|
159
|
+
self.cell(
|
|
160
|
+
w=cell.width,
|
|
161
|
+
h=cell.height,
|
|
162
|
+
txt=cell.text,
|
|
163
|
+
ln=cell.has_break,
|
|
164
|
+
fill=cell.has_fill,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _justify_line(self, cells: list[Cell], memory_line: list[str]) -> list[Cell]:
|
|
168
|
+
width = self.w - self.l_margin - self.r_margin
|
|
169
|
+
clean_line_w = 0
|
|
170
|
+
for cell in cells:
|
|
171
|
+
cursor = cell.cursor
|
|
172
|
+
self.set_font(cursor.family, cursor.style, cursor.size)
|
|
173
|
+
cell.width = self.get_string_width(cell.text)
|
|
174
|
+
if cell.text != " ":
|
|
175
|
+
clean_line_w += cell.width
|
|
176
|
+
|
|
177
|
+
char_check = any([char in memory_line for char in ["=", "#"]])
|
|
178
|
+
if memory_line.count(" ") and not char_check:
|
|
179
|
+
space_width = (width - clean_line_w) / memory_line.count(" ")
|
|
180
|
+
for cell in cells:
|
|
181
|
+
if cell.text == " ":
|
|
182
|
+
cell.width = space_width
|
|
183
|
+
|
|
184
|
+
for cell in cells:
|
|
185
|
+
if cell.text == "^":
|
|
186
|
+
cell.text = " "
|
|
187
|
+
|
|
188
|
+
return cells
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from bookcraft.cell.rules.BreakLineRule import BreakLineRule
|
|
4
|
+
from bookcraft.cell.rules.CharRule import CharRule
|
|
5
|
+
from bookcraft.cell.rules.HeaderSpacingRule import HeaderSpacingRule
|
|
6
|
+
from bookcraft.cell.rules.Rule import CellRule, CellSpecificationRule
|
|
7
|
+
from bookcraft.models import Cell, Context
|
|
8
|
+
from bookcraft.specs.FirstLineSpecification import FirstLineSpecification
|
|
9
|
+
from bookcraft.specs.IsCharEqualSpecification import IsCharEqualSpecification
|
|
10
|
+
from bookcraft.specs.IsEndOfLineSpecification import IsEndOfLineSpecification
|
|
11
|
+
from bookcraft.specs.LineHasCharSpecification import LineHasCharSpecification
|
|
12
|
+
from bookcraft.specs.Specification import AndSpecification, NotSpecification
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CellFactory:
|
|
17
|
+
rules: tuple[CellRule, ...] = (
|
|
18
|
+
CellSpecificationRule(
|
|
19
|
+
AndSpecification(
|
|
20
|
+
NotSpecification(LineHasCharSpecification(["#"], -1)),
|
|
21
|
+
IsCharEqualSpecification(["#"]),
|
|
22
|
+
NotSpecification(FirstLineSpecification()),
|
|
23
|
+
),
|
|
24
|
+
HeaderSpacingRule(),
|
|
25
|
+
),
|
|
26
|
+
CellSpecificationRule(
|
|
27
|
+
NotSpecification(
|
|
28
|
+
IsCharEqualSpecification(["#", ">", "$", "<", "=", "%", "&"])
|
|
29
|
+
),
|
|
30
|
+
CharRule(),
|
|
31
|
+
),
|
|
32
|
+
CellSpecificationRule(
|
|
33
|
+
IsEndOfLineSpecification(),
|
|
34
|
+
BreakLineRule(),
|
|
35
|
+
),
|
|
36
|
+
CellSpecificationRule(
|
|
37
|
+
AndSpecification(
|
|
38
|
+
IsEndOfLineSpecification(),
|
|
39
|
+
LineHasCharSpecification(["#"], 0),
|
|
40
|
+
NotSpecification(LineHasCharSpecification(["#"], 1)),
|
|
41
|
+
),
|
|
42
|
+
HeaderSpacingRule(),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def create_cells(self, context: Context) -> list[Cell]:
|
|
47
|
+
cells = []
|
|
48
|
+
for rule in self.rules:
|
|
49
|
+
cell = rule.apply(context)
|
|
50
|
+
|
|
51
|
+
if cell is None:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
cells.append(cell)
|
|
55
|
+
|
|
56
|
+
return cells
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from bookcraft.cell.rules.Rule import CellRule
|
|
2
|
+
from bookcraft.models import Cell, Context
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BreakLineRule(CellRule):
|
|
6
|
+
def apply(self, context: Context) -> Cell:
|
|
7
|
+
CONFIG = context.config
|
|
8
|
+
return Cell(
|
|
9
|
+
width=0,
|
|
10
|
+
height=CONFIG.DEFAULT_HEIGHT,
|
|
11
|
+
text="",
|
|
12
|
+
has_break=True,
|
|
13
|
+
cursor=context.cursor,
|
|
14
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from bookcraft.cell.rules.Rule import CellRule
|
|
2
|
+
from bookcraft.models import Cell, Context
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CharRule(CellRule):
|
|
6
|
+
def apply(self, context: Context) -> Cell:
|
|
7
|
+
CONFIG = context.config
|
|
8
|
+
page = context.memory[0]
|
|
9
|
+
char = page[context.i][context.j]
|
|
10
|
+
|
|
11
|
+
return Cell(
|
|
12
|
+
width=0,
|
|
13
|
+
height=CONFIG.DEFAULT_HEIGHT,
|
|
14
|
+
text=char,
|
|
15
|
+
cursor=context.cursor,
|
|
16
|
+
has_fill=True,
|
|
17
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from bookcraft.cell.rules.Rule import CellRule
|
|
2
|
+
from bookcraft.models import Cell, Context
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class HeaderSpacingRule(CellRule):
|
|
6
|
+
def apply(self, context: Context) -> Cell:
|
|
7
|
+
CONFIG = context.config
|
|
8
|
+
return Cell(
|
|
9
|
+
width=0,
|
|
10
|
+
height=CONFIG.HEADING_SPACING_HEIGHT,
|
|
11
|
+
text="",
|
|
12
|
+
has_break=True,
|
|
13
|
+
cursor=context.cursor,
|
|
14
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from bookcraft.models import Cell, Context
|
|
5
|
+
from bookcraft.specs.Specification import Specification
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CellRule(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def apply(self, context: Context) -> Cell:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CellSpecificationRule(CellRule):
|
|
15
|
+
def __init__(self, specification: Specification, rule: CellRule) -> None:
|
|
16
|
+
self.specification = specification
|
|
17
|
+
self.rule = rule
|
|
18
|
+
|
|
19
|
+
def apply(self, context: Context) -> Optional[Cell]:
|
|
20
|
+
if self.specification.is_satisfied(context):
|
|
21
|
+
return self.rule.apply(context)
|
|
22
|
+
|
|
23
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from bookcraft._generate import generate, _SWITCH_MAP
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
parser = argparse.ArgumentParser(description="Generate a PDF book with bookcraft.")
|
|
8
|
+
parser.add_argument("mode", choices=list(_SWITCH_MAP), help="Rendering mode")
|
|
9
|
+
parser.add_argument("--books", required=True, help="Path to books directory")
|
|
10
|
+
parser.add_argument("--settings", required=True, help="Path to settings YAML")
|
|
11
|
+
parser.add_argument("--fonts", required=True, help="Path to fonts YAML")
|
|
12
|
+
parser.add_argument("--keywords", required=True, help="Path to keywords YAML")
|
|
13
|
+
parser.add_argument("--output", required=True, help="Output PDF path")
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
generate(
|
|
17
|
+
books_path=args.books,
|
|
18
|
+
settings_path=args.settings,
|
|
19
|
+
fonts_path=args.fonts,
|
|
20
|
+
keywords_path=args.keywords,
|
|
21
|
+
output_path=args.output,
|
|
22
|
+
mode=args.mode,
|
|
23
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Config:
|
|
8
|
+
BOOKS_PATH: str
|
|
9
|
+
FONTS: dict
|
|
10
|
+
SETTINGS: dict
|
|
11
|
+
KEYWORDS: list
|
|
12
|
+
switch: str # "c-", "c-dark-", "ra-", "ra-dark-"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def is_dark(self) -> bool:
|
|
16
|
+
return "dark" in self.switch
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def is_ra(self) -> bool:
|
|
20
|
+
return "ra" in self.switch
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def PAGE(self) -> dict:
|
|
24
|
+
return self.SETTINGS.get("page")
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def COLOUR(self) -> dict:
|
|
28
|
+
return self.SETTINGS.get("colour")
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def ROLES(self) -> dict:
|
|
32
|
+
return self.SETTINGS.get("roles")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def TEXT(self) -> dict:
|
|
36
|
+
return self.SETTINGS.get("text")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def DEFAULT_CURSOR(self) -> dict:
|
|
40
|
+
return {
|
|
41
|
+
"family": self.TEXT["default"]["cursor"]["family"],
|
|
42
|
+
"style": self.TEXT["default"]["cursor"]["style"],
|
|
43
|
+
"size": self.TEXT["default"]["cursor"]["size"],
|
|
44
|
+
"colour": self.TEXT["default"]["cursor"]["colour"],
|
|
45
|
+
"fill": self.TEXT["default"]["cursor"]["fill"],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def DEFAULT_FONT(self) -> dict:
|
|
50
|
+
return {
|
|
51
|
+
"family": self.TEXT["default"]["cursor"]["family"],
|
|
52
|
+
"style": self.TEXT["default"]["cursor"]["style"],
|
|
53
|
+
"size": self.TEXT["default"]["cursor"]["size"],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def DEFAULT_HEIGHT(self) -> dict:
|
|
58
|
+
return self.TEXT["default"]["height"]
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def DEFAULT_FILL(self) -> dict:
|
|
62
|
+
return self.TEXT["default"]["cursor"]["fill"]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def TEMPLATE_FONT(self) -> dict:
|
|
66
|
+
return {
|
|
67
|
+
"family": self.TEXT["template"]["cursor"]["family"],
|
|
68
|
+
"style": self.TEXT["template"]["cursor"]["style"],
|
|
69
|
+
"size": self.TEXT["template"]["cursor"]["size"],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def TEMPLATE_COLOR(self) -> dict:
|
|
74
|
+
return self.TEXT["template"]["cursor"]["colour"]
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def TEMPLATE_HEIGHT(self) -> int:
|
|
78
|
+
return self.TEXT["template"]["height"]
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def BOLD_CURSOR(self) -> dict:
|
|
82
|
+
return {
|
|
83
|
+
"family": self.TEXT["bold"]["cursor"]["family"],
|
|
84
|
+
"style": self.TEXT["bold"]["cursor"]["style"],
|
|
85
|
+
"size": self.TEXT["bold"]["cursor"]["size"],
|
|
86
|
+
"colour": self.TEXT["bold"]["cursor"]["colour"],
|
|
87
|
+
"fill": self.TEXT["bold"]["cursor"]["fill"],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def ITALIC_CURSOR(self) -> dict:
|
|
92
|
+
return {
|
|
93
|
+
"family": self.TEXT["italic"]["cursor"]["family"],
|
|
94
|
+
"style": self.TEXT["italic"]["cursor"]["style"],
|
|
95
|
+
"size": self.TEXT["italic"]["cursor"]["size"],
|
|
96
|
+
"colour": self.TEXT["italic"]["cursor"]["colour"],
|
|
97
|
+
"fill": self.TEXT["italic"]["cursor"]["fill"],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def HEADING_CURSOR(self) -> dict:
|
|
102
|
+
return {
|
|
103
|
+
"family": self.TEXT["heading"]["cursor"]["family"],
|
|
104
|
+
"style": self.TEXT["heading"]["cursor"]["style"],
|
|
105
|
+
"size": self.TEXT["heading"]["cursor"]["size"],
|
|
106
|
+
"colour": self.TEXT["heading"]["cursor"]["colour"],
|
|
107
|
+
"fill": self.TEXT["heading"]["cursor"]["fill"],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def HEADING_SPACING_HEIGHT(self) -> int:
|
|
112
|
+
return self.TEXT["heading_spacing"]["height"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_config(
|
|
116
|
+
books_path: str,
|
|
117
|
+
settings_path: str,
|
|
118
|
+
fonts_path: str,
|
|
119
|
+
keywords_path: str,
|
|
120
|
+
switch: str,
|
|
121
|
+
) -> Config:
|
|
122
|
+
with open(fonts_path, "r") as f:
|
|
123
|
+
fonts = yaml.safe_load(f).get("fonts")
|
|
124
|
+
with open(settings_path, "r") as f:
|
|
125
|
+
settings = yaml.safe_load(f)
|
|
126
|
+
with open(keywords_path, "r") as f:
|
|
127
|
+
keywords = yaml.safe_load(f).get("keywords")
|
|
128
|
+
return Config(
|
|
129
|
+
BOOKS_PATH=books_path,
|
|
130
|
+
FONTS=fonts,
|
|
131
|
+
SETTINGS=settings,
|
|
132
|
+
KEYWORDS=keywords,
|
|
133
|
+
switch=switch,
|
|
134
|
+
)
|