booksmith 1.0.0
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.
- checksums.yaml +7 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +306 -0
- data/Rakefile +3 -0
- data/bin/booksmith +4 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/booksmith.gemspec +35 -0
- data/lib/booksmith/blockquotes.rb +228 -0
- data/lib/booksmith/book_generator.rb +313 -0
- data/lib/booksmith/epub_generator.rb +681 -0
- data/lib/booksmith/font_paths.rb +56 -0
- data/lib/booksmith/headers.rb +97 -0
- data/lib/booksmith/images.rb +92 -0
- data/lib/booksmith/lists.rb +62 -0
- data/lib/booksmith/text.rb +56 -0
- data/lib/booksmith/version.rb +5 -0
- data/lib/booksmith.rb +218 -0
- data/lib/fonts/NotoSans-Bold.ttf +0 -0
- data/lib/fonts/NotoSans-BoldItalic.ttf +0 -0
- data/lib/fonts/NotoSans-Italic.ttf +0 -0
- data/lib/fonts/NotoSans-Regular.ttf +0 -0
- data/lib/fonts/NotoSansMono-Bold.ttf +0 -0
- data/lib/fonts/NotoSansMono-Regular.ttf +0 -0
- data/lib/templates/01_introduction.md +58 -0
- data/lib/templates/book.yml +99 -0
- data/lib/templates/clock.png +0 -0
- metadata +167 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 38b21a8ba67192f56f7796f1eb76cffe35515449ae9ca7c3a53f20b1aea1d710
|
|
4
|
+
data.tar.gz: 017caa797d013fcd07cf3d0bbd54501a0e23fd5ed7fa273b7ebb91a19ba54474
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e00830e76a4e9e30679a1681c5f8b82063bd075d5e86c1bf3a4db398f6ef38249b511bdbee279292303379141a9fee03ab4278b5c1da7380cca73fed61ad5b4d
|
|
7
|
+
data.tar.gz: 8f324bc51843471ade117b2eeb21a9d8453d94b0c658f655a1f7aea64e441363a82e00422bf405d135e9dd21fc5a9eabdc922566e5b067c6f47f5756d347bdd7
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leonid Medovyy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Booksmith
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/booksmith)
|
|
4
|
+
[](https://www.ruby-lang.org)
|
|
5
|
+
[](https://kdp.amazon.com)
|
|
6
|
+
[](LICENSE.txt)
|
|
7
|
+
|
|
8
|
+
> **The production-grade, developer-first publishing engine that transforms Markdown chapters into store-ready Paperback, Hardcover, and Kindle eBook (EPUB) editions.**
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Self-publishing technical guides, fiction, or non-fiction on **Amazon Kindle Direct Publishing (KDP)** usually requires painful manual formatting in Word/InDesign or fragile HTML-to-PDF scripts that fail Amazon's automated interior print inspection.
|
|
15
|
+
|
|
16
|
+
**Booksmith** solves this with a purpose-built, dual-engine compilation pipeline. Written in Ruby, Booksmith reads clean Markdown and compiles it into:
|
|
17
|
+
|
|
18
|
+
- **Print-Ready PDFs** configured with Amazon KDP-compliant margins, geometry, vector typography, running headers, and dynamically resolved tables of contents.
|
|
19
|
+
- **Reflowable EPUB 3 eBooks** bundled with embedded typography, validated navigation manifests, and custom callout styling ready for Kindle ingestion.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🏆 Battle-Tested in Production
|
|
24
|
+
|
|
25
|
+
Booksmith isn't a theoretical proof-of-concept—it powers live, commercial titles available on Amazon worldwide across **Paperback**, **Hardcover**, and **Kindle eBook** formats:
|
|
26
|
+
|
|
27
|
+
| Title | Formats Published | Store Link |
|
|
28
|
+
| :--- | :--- | :--- |
|
|
29
|
+
| **Modern Applications of Ninjutsu** | Hardcover • Kindle | [View on Amazon](https://www.amazon.com/dp/B0HNF1T788/) |
|
|
30
|
+
| **Jealousy in the Jungle: Invitation & Rival's Awakening** | Hardcover • Kindle | [View on Amazon](https://www.amazon.com/Jealousy-Jungle-Invitation-Rivals-Awakening/dp/B0DY872FHM/) |
|
|
31
|
+
|
|
32
|
+
> [!NOTE]
|
|
33
|
+
> Both titles passed Amazon KDP's automated preflight review, interior gutter/margin validation, and reflowable eBook ingest with **zero formatting rejections**.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ✨ Key Features
|
|
38
|
+
|
|
39
|
+
- **Dual-Target Compilation**: Generate both print-ready PDF and Kindle-compliant EPUB simultaneously with a single CLI command (`booksmith build --format all`).
|
|
40
|
+
- **Two-Pass Dynamic Pagination**: Calculates exact chapter starting pages and table-of-contents page offsets automatically prior to final PDF generation.
|
|
41
|
+
- **Self-Contained Typography**: Bundles Google's **Noto Sans** and **Noto Sans Mono** TrueType font families. No external font dependencies or OS-level font installations required.
|
|
42
|
+
- **Rich Callout Blocks**:
|
|
43
|
+
- **Standard Callouts**: Automatically styles bold headers into shaded callout blocks.
|
|
44
|
+
- **Alternate Callouts (`{: .alt_blockquote}`)**: Tinted background containers for sidebars and tips.
|
|
45
|
+
- **Exercise Callouts (`{: .exercise_blockquote}`)**: Callout cards with customizable asset icons (e.g., timer/clock) and point-level vertical alignment nudges.
|
|
46
|
+
- **Print Layout Control**:
|
|
47
|
+
- `<!-- subtitle: ... -->` for clean chapter metadata without breaking Markdown linters.
|
|
48
|
+
- `<!-- PAGE_BREAK -->` comments for manual page control across physical print layouts.
|
|
49
|
+
- **Automated Image Pipeline**: Parses inline Markdown sizing attributes (`"width=350 top=10 bottom=10"`) and rasterizes transparent PNG backgrounds via ImageMagick to prevent black-box PDF rendering artifacts.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🏗️ Architecture & Engineering Highlights
|
|
54
|
+
|
|
55
|
+
Booksmith was engineered with a modular, extensible architecture that separates AST parsing, layout geometry calculations, and format-specific document packaging.
|
|
56
|
+
|
|
57
|
+
```mermaid
|
|
58
|
+
flowchart TD
|
|
59
|
+
MD["Markdown Chapters (chapters/*.md)"] --> AST["Kramdown AST Parser & Normalizer"]
|
|
60
|
+
CFG["Configuration (config/book.yml)"] --> AST
|
|
61
|
+
|
|
62
|
+
subgraph PDF_Pipeline ["Print Engine: Prawn (2-Pass Layout)"]
|
|
63
|
+
AST --> P1["Pass 1: Render Dry-Run & Measure Offsets"]
|
|
64
|
+
P1 --> TOC["Compute TOC Height & Page Shift Offset"]
|
|
65
|
+
TOC --> P2["Pass 2: Final Vector Rendering & Running Pagination"]
|
|
66
|
+
P2 --> PDF_OUT["book.pdf (Paperback & Hardcover Ready)"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
subgraph EPUB_Pipeline ["eBook Engine: GEPUB (EPUB 3 / XHTML)"]
|
|
70
|
+
AST --> XGEN["XHTML 1.1 Document Generator & CSS Styler"]
|
|
71
|
+
XGEN --> SPN["Embed Fonts, Assets & Build EPUB Spine"]
|
|
72
|
+
SPN --> EPUB_OUT["book.epub (Kindle KDP Ready)"]
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Engineering Design Highlights
|
|
77
|
+
|
|
78
|
+
#### 1. Two-Pass PDF Pagination & TOC Resolution
|
|
79
|
+
|
|
80
|
+
Generating a table of contents in print documents presents a circular dependency: adding TOC entries can push chapter pages down, altering the page numbers that the TOC itself displays. Booksmith resolves this with a deterministic two-pass algorithm:
|
|
81
|
+
|
|
82
|
+
1. **Pass 1 (Measurement)**: Traverses the Kramdown AST in an in-memory Prawn buffer to compute exact chapter heights, section boundaries, and TOC page space.
|
|
83
|
+
2. **Pass 2 (Synthesis)**: Applies the computed TOC page offset, injects the generated Table of Contents, formats running page numbers (`page_content_bottom_gap`), and writes the final vector PDF.
|
|
84
|
+
|
|
85
|
+
#### 2. Vector Typography & Geometry Engine
|
|
86
|
+
|
|
87
|
+
Instead of relying on web-view wrappers or headless browsers, Booksmith renders directly to PDF primitives using `Prawn`. Text leading, bullet indentations (`list_bullet_indent`, `list_marker_gap`), and callout paddings are calculated in points, producing crisp vector type that meets high-resolution commercial print standards (300+ DPI).
|
|
88
|
+
|
|
89
|
+
#### 3. Dual-Format AST Translation
|
|
90
|
+
|
|
91
|
+
Booksmith normalizes Markdown elements into an intermediate AST via Kramdown, then branches execution:
|
|
92
|
+
|
|
93
|
+
- **PDF Converter** (`Booksmith::PDFConverter`): Translates elements into low-level Prawn cursor calls, handling bounding boxes, font switching, and multi-column icon callouts.
|
|
94
|
+
- **EPUB Generator** (`Booksmith::EpubGenerator`): Serializes nodes into strict XHTML, injects embedded CSS, registers the TrueType fonts in the OPF manifest, and packages the complete EPUB 3 archive via GEPUB.
|
|
95
|
+
|
|
96
|
+
#### 4. Automated Image Pre-processing & Alpha Flattening
|
|
97
|
+
|
|
98
|
+
PDF renderers and commercial print presses frequently misrender PNG alpha transparency as opaque black blocks. Booksmith reads configuration-driven background colors (`image_flatten_bg_color`) and utilizes ImageMagick to composite transparent images against the target page tone before embedding.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🚀 Quickstart
|
|
103
|
+
|
|
104
|
+
### 1. Installation
|
|
105
|
+
|
|
106
|
+
Install Booksmith via RubyGems:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
gem install booksmith
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Or add it to your project's `Gemfile`:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
gem 'booksmith', '~> 1.0'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Initialize a Project
|
|
119
|
+
|
|
120
|
+
Create a new book directory and initialize the scaffolding:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
mkdir my-masterpiece
|
|
124
|
+
cd my-masterpiece
|
|
125
|
+
booksmith init
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This generates the standard project structure:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
my-masterpiece/
|
|
132
|
+
├── assets/
|
|
133
|
+
│ └── clock.png # Sample exercise callout icon
|
|
134
|
+
├── chapters/
|
|
135
|
+
│ └── 01_introduction.md # Starter chapter template
|
|
136
|
+
└── config/
|
|
137
|
+
└── book.yml # Master typography, metadata & layout config
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3. Build Your Book
|
|
141
|
+
|
|
142
|
+
Compile your chapters into your target formats:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
# Generate Paperback/Hardcover PDF (default: book.pdf)
|
|
146
|
+
booksmith build
|
|
147
|
+
|
|
148
|
+
# Generate Kindle-ready EPUB (book.epub)
|
|
149
|
+
booksmith build --epub
|
|
150
|
+
|
|
151
|
+
# Generate both formats simultaneously
|
|
152
|
+
booksmith build --format all
|
|
153
|
+
|
|
154
|
+
# Build PDF without cover page
|
|
155
|
+
booksmith build --no-cover
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 4. Inspect Registered Fonts
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
booksmith fonts
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## ⚙️ Configuration Reference
|
|
167
|
+
|
|
168
|
+
All book layout options, metadata, and typography parameters are managed in `config/book.yml`:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
---
|
|
172
|
+
# Book Metadata
|
|
173
|
+
:title: "Modern Applications of Ninjutsu"
|
|
174
|
+
:author: "Authored by Leonid Medovyy"
|
|
175
|
+
|
|
176
|
+
# Font Typography
|
|
177
|
+
:title_font: "Noto Sans Mono"
|
|
178
|
+
:author_font: "Noto Sans Mono"
|
|
179
|
+
:header_font: "Noto Sans"
|
|
180
|
+
:subtitle_font: "Noto Sans Mono"
|
|
181
|
+
:body_font: "Noto Sans"
|
|
182
|
+
|
|
183
|
+
# Print & Page Geometry
|
|
184
|
+
:blank_page_after_cover: true
|
|
185
|
+
:table_of_contents: true
|
|
186
|
+
:font_size: 10.0
|
|
187
|
+
:line_height: 1.5
|
|
188
|
+
:page_numbers: true
|
|
189
|
+
:page_number_font_size: 9
|
|
190
|
+
:page_content_bottom_gap: 30
|
|
191
|
+
|
|
192
|
+
# Header Sizing (Points)
|
|
193
|
+
:title_font_size: 24
|
|
194
|
+
:author_font_size: 18
|
|
195
|
+
:h1_font_size: 20
|
|
196
|
+
:h2_font_size: 18
|
|
197
|
+
:h3_font_size: 14
|
|
198
|
+
:h4_font_size: 12
|
|
199
|
+
|
|
200
|
+
# Callout Boxes & Blockquotes
|
|
201
|
+
:blockquote_bg_color: "EEEEEE"
|
|
202
|
+
:blockquote_padding: 12
|
|
203
|
+
:blockquote_margin_top: 10
|
|
204
|
+
:blockquote_margin_bottom: 10
|
|
205
|
+
|
|
206
|
+
# Exercise Callout with Custom Icon
|
|
207
|
+
:exercise_blockquote_bg_color: "EEF3FF"
|
|
208
|
+
:exercise_blockquote_image: "assets/clock.png"
|
|
209
|
+
:exercise_blockquote_image_width: 18
|
|
210
|
+
:exercise_blockquote_image_height: 18
|
|
211
|
+
:exercise_blockquote_image_voffset: -2
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## ✍️ Markdown Authoring Guide
|
|
217
|
+
|
|
218
|
+
Booksmith supports full GitHub Flavored Markdown with custom layout enhancements:
|
|
219
|
+
|
|
220
|
+
### Chapter Titles & Subtitles
|
|
221
|
+
|
|
222
|
+
Use standard `#` for chapter titles, followed immediately by an invisible subtitle comment:
|
|
223
|
+
|
|
224
|
+
```markdown
|
|
225
|
+
# CHAPTER I
|
|
226
|
+
<!-- subtitle: GETTING STARTED WITH BOOKSMITH -->
|
|
227
|
+
|
|
228
|
+
Chapter opening text starts here...
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Manual Page Breaks
|
|
232
|
+
|
|
233
|
+
Insert a clean page break anywhere in your manuscript without adding visible artifacts in Markdown viewers:
|
|
234
|
+
|
|
235
|
+
```markdown
|
|
236
|
+
<!-- PAGE_BREAK -->
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Styled Callout Boxes
|
|
240
|
+
|
|
241
|
+
```markdown
|
|
242
|
+
> **Pro Tip**
|
|
243
|
+
>
|
|
244
|
+
> Standard callouts automatically elevate bold first lines into header titles.
|
|
245
|
+
|
|
246
|
+
> **Sidebar Note**
|
|
247
|
+
>
|
|
248
|
+
> Alternate shaded callout box for supplementary commentary.
|
|
249
|
+
{: .alt_blockquote}
|
|
250
|
+
|
|
251
|
+
> Hands-On Challenge
|
|
252
|
+
>
|
|
253
|
+
> Exercise callout displaying the configured icon alongside the prompt:
|
|
254
|
+
> - Define the core thesis of your publication
|
|
255
|
+
> - Configure print trim sizes in book.yml
|
|
256
|
+
{: .exercise_blockquote}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Sized & Centered Images
|
|
260
|
+
|
|
261
|
+
Specify dimensions and vertical padding directly in image Markdown:
|
|
262
|
+
|
|
263
|
+
```markdown
|
|
264
|
+

|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 📊 Feature Comparison
|
|
270
|
+
|
|
271
|
+
| Feature | Booksmith | Pandoc / LaTeX | Word / InDesign |
|
|
272
|
+
| :--- | :---: | :---: | :---: |
|
|
273
|
+
| **KDP Print-Ready Out of the Box** | ✅ Yes | ⚠️ Requires Config | ❌ Manual Formatting |
|
|
274
|
+
| **Simultaneous PDF & EPUB Output** | ✅ Yes | ⚠️ Separate Pipelines | ❌ Separate Exports |
|
|
275
|
+
| **Dynamic 2-Pass TOC Resolution** | ✅ Built-in | ⚠️ Complex LaTeX Runs | ❌ Manual TOC Updates |
|
|
276
|
+
| **Zero External Font Dependencies** | ✅ Bundled TTF | ❌ System Fonts | ❌ System Fonts |
|
|
277
|
+
| **Markdown Linter Safe Comments** | ✅ Yes (`<!-- -->`) | ❌ LaTeX Injections | ❌ N/A |
|
|
278
|
+
| **Transparent PNG Flattening** | ✅ Automated | ❌ Manual Preprocessing | ⚠️ Manual Flattening |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 🛠️ CLI Command Reference
|
|
283
|
+
|
|
284
|
+
| Command | Description |
|
|
285
|
+
| :--- | :--- |
|
|
286
|
+
| `booksmith init` | Scaffolds a new book workspace with directories, default assets, sample chapter, and `book.yml`. |
|
|
287
|
+
| `booksmith build` | Builds `book.pdf` with Amazon KDP print specifications. |
|
|
288
|
+
| `booksmith build --epub` | Builds `book.epub` with validated navigation and embedded typography. |
|
|
289
|
+
| `booksmith build --format all` | Compiles both PDF and EPUB in a single pass. |
|
|
290
|
+
| `booksmith build --no-cover` | Omits the title/cover page from generation. |
|
|
291
|
+
| `booksmith fonts` | Lists all registered font families available for document rendering. |
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 👤 Author
|
|
296
|
+
|
|
297
|
+
Crafted by **Leonid Medovyy** ([@leouofa](https://github.com/leouofa)).
|
|
298
|
+
|
|
299
|
+
- **GitHub**: [@leouofa](https://github.com/leouofa)
|
|
300
|
+
- **Gem**: [rubygems.org/gems/booksmith](https://rubygems.org/gems/booksmith)
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## đź“„ License
|
|
305
|
+
|
|
306
|
+
This project is open-source under the [MIT License](LICENSE.txt).
|
data/Rakefile
ADDED
data/bin/booksmith
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/booksmith.gemspec
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'booksmith/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'booksmith'
|
|
9
|
+
spec.version = Booksmith::VERSION
|
|
10
|
+
spec.authors = ['Leonid Medovyy']
|
|
11
|
+
spec.email = ['leonid@storypro.io']
|
|
12
|
+
|
|
13
|
+
spec.summary = 'A flexible Markdown-to-PDF and EPUB book publishing tool'
|
|
14
|
+
spec.description = 'Converts Markdown chapters into a styled PDF or EPUB book using Prawn, GEPUB, and Kramdown.'
|
|
15
|
+
spec.homepage = 'https://github.com/leouofa/booksmith'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
18
|
+
|
|
19
|
+
spec.files = Dir.chdir(__dir__) do
|
|
20
|
+
Dir.glob('{bin,lib}/**/*').select { |f| File.file?(f) } +
|
|
21
|
+
%w[LICENSE.txt README.md Rakefile Gemfile booksmith.gemspec].select { |f| File.file?(f) }
|
|
22
|
+
end
|
|
23
|
+
spec.bindir = 'bin'
|
|
24
|
+
spec.executables = ['booksmith']
|
|
25
|
+
spec.require_paths = ['lib']
|
|
26
|
+
|
|
27
|
+
spec.add_dependency 'commander', '>= 4.6'
|
|
28
|
+
spec.add_dependency 'gepub', '>= 1.0'
|
|
29
|
+
spec.add_dependency 'kramdown', '>= 2.4'
|
|
30
|
+
spec.add_dependency 'ostruct'
|
|
31
|
+
spec.add_dependency 'prawn', '>= 2.4'
|
|
32
|
+
|
|
33
|
+
spec.add_development_dependency 'bundler', '>= 2.0'
|
|
34
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
|
35
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
module Booksmith
|
|
2
|
+
module Blockquotes
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def render_blockquote(el, style_name)
|
|
6
|
+
bg_color = blockquote_style_value(style_name, :bg_color, 'EEEEEE')
|
|
7
|
+
padding_left = blockquote_padding(style_name, :left)
|
|
8
|
+
padding_right = blockquote_padding(style_name, :right)
|
|
9
|
+
padding_top = blockquote_padding(style_name, :top)
|
|
10
|
+
padding_bottom = blockquote_padding(style_name, :bottom)
|
|
11
|
+
margin_top = blockquote_style_value(style_name, :margin_top, 10).to_f
|
|
12
|
+
margin_bot = blockquote_style_value(style_name, :margin_bottom, 10).to_f
|
|
13
|
+
content_width = @pdf.bounds.width - padding_left - padding_right
|
|
14
|
+
header_el, body_els = split_blockquote_children(el)
|
|
15
|
+
|
|
16
|
+
@pdf.move_down margin_top
|
|
17
|
+
|
|
18
|
+
box_height = measure_blockquote_height(el, content_width, style_name) + padding_top + padding_bottom
|
|
19
|
+
|
|
20
|
+
saved = @pdf.fill_color
|
|
21
|
+
@pdf.fill_color bg_color
|
|
22
|
+
@pdf.fill_rectangle([@pdf.bounds.left, @pdf.cursor], @pdf.bounds.width, box_height)
|
|
23
|
+
@pdf.fill_color saved
|
|
24
|
+
|
|
25
|
+
@pdf.move_down padding_top
|
|
26
|
+
with_image_flatten_bg(blockquote_image_flatten_color(style_name)) do
|
|
27
|
+
@pdf.indent(padding_left, padding_right) do
|
|
28
|
+
render_blockquote_title_row(header_el, style_name) if header_el
|
|
29
|
+
body_els.each { |child| convert_element(child) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
@pdf.move_down padding_bottom
|
|
33
|
+
@pdf.move_down margin_bot
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render_exercise_blockquote(el)
|
|
37
|
+
bg_color = blockquote_style_value(:exercise_blockquote, :bg_color, 'EEF3FF')
|
|
38
|
+
padding_left = blockquote_padding(:exercise_blockquote, :left)
|
|
39
|
+
padding_right = blockquote_padding(:exercise_blockquote, :right)
|
|
40
|
+
padding_top = blockquote_padding(:exercise_blockquote, :top)
|
|
41
|
+
padding_bottom = blockquote_padding(:exercise_blockquote, :bottom)
|
|
42
|
+
margin_top = blockquote_style_value(:exercise_blockquote, :margin_top, 12).to_f
|
|
43
|
+
margin_bot = blockquote_style_value(:exercise_blockquote, :margin_bottom, 12).to_f
|
|
44
|
+
content_width = @pdf.bounds.width - padding_left - padding_right
|
|
45
|
+
|
|
46
|
+
header_el, body_els = split_exercise_children(el)
|
|
47
|
+
|
|
48
|
+
@pdf.move_down margin_top
|
|
49
|
+
|
|
50
|
+
box_height = measure_exercise_height(content_width, header_el, body_els) + padding_top + padding_bottom
|
|
51
|
+
top_cursor = @pdf.cursor
|
|
52
|
+
|
|
53
|
+
saved = @pdf.fill_color
|
|
54
|
+
@pdf.fill_color bg_color
|
|
55
|
+
@pdf.fill_rectangle([@pdf.bounds.left, top_cursor], @pdf.bounds.width, box_height)
|
|
56
|
+
@pdf.fill_color saved
|
|
57
|
+
|
|
58
|
+
@pdf.move_down padding_top
|
|
59
|
+
with_image_flatten_bg(blockquote_image_flatten_color(:exercise_blockquote)) do
|
|
60
|
+
@pdf.indent(padding_left, padding_right) do
|
|
61
|
+
render_exercise_title_row(header_el)
|
|
62
|
+
body_els.each { |child| convert_element(child) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
@pdf.move_down padding_bottom
|
|
66
|
+
@pdf.move_down margin_bot
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def alt_blockquote?(element)
|
|
70
|
+
classes = element.attr.fetch('class', '').split
|
|
71
|
+
classes.include?('alt_blockquote')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def exercise_blockquote?(element)
|
|
75
|
+
element.attr.fetch('class', '').split.include?('exercise_blockquote')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def blockquote_header_paragraph?(element)
|
|
79
|
+
return false unless element&.type == :p
|
|
80
|
+
|
|
81
|
+
inline = element.children.reject { |c| c.type == :blank || (c.type == :text && c.value.strip.empty?) }
|
|
82
|
+
inline.length == 1 && inline.first.type == :strong
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def split_blockquote_children(el)
|
|
86
|
+
children = el.children.reject { |c| c.type == :blank }
|
|
87
|
+
return [nil, children] if children.empty?
|
|
88
|
+
return [children.first, children.drop(1)] if blockquote_header_paragraph?(children.first)
|
|
89
|
+
|
|
90
|
+
[nil, children]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def split_exercise_children(el)
|
|
94
|
+
children = el.children.reject { |c| c.type == :blank }
|
|
95
|
+
[children.first, children.drop(1)]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def blockquote_style_value(style_name, setting, default)
|
|
99
|
+
key = :"#{style_name}_#{setting}"
|
|
100
|
+
fallback_key = :"blockquote_#{setting}"
|
|
101
|
+
|
|
102
|
+
return @config[key] if @config.key?(key)
|
|
103
|
+
return @config[fallback_key] if style_name != :blockquote && @config.key?(fallback_key)
|
|
104
|
+
|
|
105
|
+
default
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def blockquote_padding(style_name, side)
|
|
109
|
+
per_side_key = :"#{style_name}_padding_#{side}"
|
|
110
|
+
return @config[per_side_key].to_f if @config.key?(per_side_key)
|
|
111
|
+
|
|
112
|
+
if style_name != :blockquote
|
|
113
|
+
fallback_per_side = :"blockquote_padding_#{side}"
|
|
114
|
+
return @config[fallback_per_side].to_f if @config.key?(fallback_per_side)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
default_padding = style_name == :exercise_blockquote ? 14 : 12
|
|
118
|
+
blockquote_style_value(style_name, :padding, default_padding).to_f
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def blockquote_header_font_size(style_name)
|
|
122
|
+
blockquote_style_value(style_name, :header_font_size, @config[:font_size] + 4).to_f
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def exercise_header_font_size
|
|
126
|
+
blockquote_header_font_size(:exercise_blockquote)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def exercise_title_row_height(img_height, title_size, header_text, title_width)
|
|
130
|
+
header_height = @pdf.height_of(header_text, size: title_size, style: :bold, width: title_width)
|
|
131
|
+
[img_height, header_height].max
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def render_blockquote_title_row(header_el, style_name)
|
|
135
|
+
header_text = plain_text(header_el)
|
|
136
|
+
title_size = blockquote_header_font_size(style_name)
|
|
137
|
+
header_gap = blockquote_style_value(style_name, :header_gap, 6).to_f
|
|
138
|
+
row_height = @pdf.height_of(header_text, size: title_size, style: :bold, width: @pdf.bounds.width)
|
|
139
|
+
|
|
140
|
+
@pdf.text_box(
|
|
141
|
+
header_text,
|
|
142
|
+
at: [@pdf.bounds.left, @pdf.cursor],
|
|
143
|
+
width: @pdf.bounds.width,
|
|
144
|
+
height: row_height,
|
|
145
|
+
size: title_size,
|
|
146
|
+
style: :bold,
|
|
147
|
+
valign: :top
|
|
148
|
+
)
|
|
149
|
+
@pdf.move_down row_height + header_gap
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def render_exercise_title_row(header_el)
|
|
153
|
+
header_text = plain_text(header_el)
|
|
154
|
+
title_size = exercise_header_font_size
|
|
155
|
+
img_width = blockquote_style_value(:exercise_blockquote, :image_width, 28).to_f
|
|
156
|
+
img_height = blockquote_style_value(:exercise_blockquote, :image_height, img_width).to_f
|
|
157
|
+
img_gap = blockquote_style_value(:exercise_blockquote, :image_gap, 8).to_f
|
|
158
|
+
img_voffset = blockquote_style_value(:exercise_blockquote, :image_voffset, 0).to_f
|
|
159
|
+
header_gap = blockquote_style_value(:exercise_blockquote, :header_gap, 6).to_f
|
|
160
|
+
title_width = [@pdf.bounds.width - img_width - img_gap, 1].max
|
|
161
|
+
row_height = exercise_title_row_height(img_height, title_size, header_text, title_width)
|
|
162
|
+
|
|
163
|
+
@pdf.bounding_box([@pdf.bounds.left, @pdf.cursor], width: @pdf.bounds.width, height: row_height) do
|
|
164
|
+
image_path = exercise_image_path
|
|
165
|
+
if image_path
|
|
166
|
+
@pdf.bounding_box([0, row_height + img_voffset], width: img_width, height: row_height) do
|
|
167
|
+
@pdf.image(image_path, fit: [img_width, img_height], position: :center, vposition: :center)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
@pdf.text_box(
|
|
172
|
+
header_text,
|
|
173
|
+
at: [img_width + img_gap, row_height],
|
|
174
|
+
width: title_width,
|
|
175
|
+
height: row_height,
|
|
176
|
+
size: title_size,
|
|
177
|
+
style: :bold,
|
|
178
|
+
valign: :center
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
@pdf.move_down header_gap
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def exercise_image_path
|
|
186
|
+
path = @config.fetch(:exercise_blockquote_image, 'assets/question_mark.png')
|
|
187
|
+
expanded = File.expand_path(path, Dir.pwd)
|
|
188
|
+
return nil unless File.exist?(expanded)
|
|
189
|
+
|
|
190
|
+
bg_color = blockquote_image_flatten_color(:exercise_blockquote)
|
|
191
|
+
prepare_png_path(expanded, background_color: bg_color)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def measure_blockquote_height(el, content_width, style_name = :blockquote)
|
|
195
|
+
header_el, body_els = split_blockquote_children(el)
|
|
196
|
+
scratch = create_scratch_document(content_width)
|
|
197
|
+
flatten_bg = blockquote_image_flatten_color(style_name)
|
|
198
|
+
converter = PDFConverter.new(scratch, @config, image_flatten_bg: flatten_bg)
|
|
199
|
+
start_y = scratch.cursor
|
|
200
|
+
if header_el
|
|
201
|
+
converter.send(:render_blockquote_title_row, header_el, style_name)
|
|
202
|
+
body_els.each { |child| converter.send(:convert_element, child) }
|
|
203
|
+
else
|
|
204
|
+
body_els.each { |child| converter.send(:convert_element, child) }
|
|
205
|
+
end
|
|
206
|
+
[start_y - scratch.cursor, 0].max
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def measure_exercise_height(content_width, header_el, body_els)
|
|
210
|
+
scratch = create_scratch_document(content_width)
|
|
211
|
+
flatten_bg = blockquote_image_flatten_color(:exercise_blockquote)
|
|
212
|
+
converter = PDFConverter.new(scratch, @config, image_flatten_bg: flatten_bg)
|
|
213
|
+
start_y = scratch.cursor
|
|
214
|
+
converter.send(:render_exercise_title_row, header_el)
|
|
215
|
+
body_els.each { |child| converter.send(:convert_element, child) }
|
|
216
|
+
[start_y - scratch.cursor, 0].max
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def create_scratch_document(content_width)
|
|
220
|
+
scratch = Prawn::Document.new(
|
|
221
|
+
page_size: [content_width + 2, 5000], margin: [0, 1, 0, 1]
|
|
222
|
+
)
|
|
223
|
+
FontPaths.register(scratch)
|
|
224
|
+
scratch.font(@config.fetch(:body_font, 'Noto Sans'))
|
|
225
|
+
scratch
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|