przn 0.1.6 → 0.3.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 +4 -4
- data/README.md +150 -2
- data/default_theme.yml +28 -9
- data/exe/przn +21 -5
- data/lib/przn/controller.rb +27 -0
- data/lib/przn/image_util.rb +24 -0
- data/lib/przn/kitty_text.rb +4 -1
- data/lib/przn/parser.rb +46 -2
- data/lib/przn/pdf_exporter.rb +76 -5
- data/lib/przn/renderer.rb +253 -68
- data/lib/przn/screenshot_pdf_exporter.rb +101 -0
- data/lib/przn/terminal.rb +4 -0
- data/lib/przn/theme.rb +19 -1
- data/lib/przn/version.rb +1 -1
- data/lib/przn.rb +21 -1
- metadata +71 -51
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b33677433cd4802ee75ef9d7eb821d2dbb6faf8b1d2364d535b6aa28bffa6eb3
|
|
4
|
+
data.tar.gz: 930ecad39096eea771759134cd6c7e4a83c55a00ae98c59ead5afea1dcc46f17
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 08eafdea3c2f55a29ccfd879b3bfa1541576be74fbb042cc33c527e39289e3dcf1375a10df033820e37f69e9f6726f580cd19ae468d633d73c9d65a23736674b
|
|
7
|
+
data.tar.gz: fcbf194e2a090de86a823fd8a0076977db73cf69f857cea6f4406f9fe31c2f44dab60120d61dbda7f10545e2fd750089df8e34233f0808fefd096bcec3cc25e1
|
data/README.md
CHANGED
|
@@ -15,15 +15,30 @@ gem install przn
|
|
|
15
15
|
przn your_slides.md
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
To open the presentation directly at a specific slide, append `@N` (1-based):
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
przn your_slides.md @42
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Out-of-range numbers are clamped to the last slide, so `@9999` jumps to the end.
|
|
25
|
+
|
|
18
26
|
### PDF export
|
|
19
27
|
|
|
28
|
+
Two flavors:
|
|
29
|
+
|
|
20
30
|
```
|
|
21
|
-
przn --export your_slides.md
|
|
31
|
+
przn --export your_slides.md # vector capture (default)
|
|
22
32
|
przn --export pdf your_slides.md
|
|
23
33
|
przn --export pdf -o output.pdf your_slides.md
|
|
34
|
+
|
|
35
|
+
przn --export prawn your_slides.md # Prawn (headless fallback)
|
|
36
|
+
przn --export prawn -o output.pdf your_slides.md
|
|
24
37
|
```
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
**`--export pdf`** (default) drives the live renderer for each slide and asks the terminal to save the rendered pane as a one-page **vector PDF**, then concatenates the per-slide PDFs into a single multi-page PDF. Output is an exact match of what's on screen — gradients, proportional fonts, OSC 66 sized text, custom bullets, all show up exactly as you'd see them — but vector, so the file stays small, scales infinitely, and text remains selectable. Requires running inside a terminal that implements the OSC 7772 `capture` command to a `.pdf` path (currently [Echoes](https://github.com/amatsuda/echoes)). The slides flicker through the visible pane during export.
|
|
40
|
+
|
|
41
|
+
**`--export prawn`** is the headless fallback: it renders the deck directly into a vector PDF via Prawn, without touching the terminal. Useful for CI or environments where Echoes isn't available, but diverges from the on-screen rendering for any feature the live renderer adds (OSC 66 sized text, OSC 7772 backgrounds, proportional fonts). Requires a TrueType font (with `glyf` outlines) for proper rendering — Prawn does not support CFF-based fonts (most `.otf` files). Fonts are auto-detected in this order: NotoSansJP TTF, HackGen, Arial Unicode.
|
|
27
42
|
|
|
28
43
|
### Key bindings
|
|
29
44
|
|
|
@@ -35,6 +50,10 @@ Requires a TrueType font (with `glyf` outlines) for proper rendering. Prawn does
|
|
|
35
50
|
| `G` | Last slide |
|
|
36
51
|
| `q` `Ctrl-C` | Quit |
|
|
37
52
|
|
|
53
|
+
### Selecting and copying text
|
|
54
|
+
|
|
55
|
+
`przn` doesn't capture mouse events, so drag-to-select and the terminal's own copy shortcut (Kitty: `Cmd+C` on macOS, `Ctrl+Shift+C` on Linux) work normally on a slide. Mouse-tracking modes that may have leaked from a previously crashed program are explicitly disabled on entry, so drag selection is reliable.
|
|
56
|
+
|
|
38
57
|
## Markdown format
|
|
39
58
|
|
|
40
59
|
przn's Markdown format is compatible with [Rabbit](https://rabbit-shocker.org/)'s Markdown mode.
|
|
@@ -62,6 +81,8 @@ more content
|
|
|
62
81
|
`inline code`
|
|
63
82
|
```
|
|
64
83
|
|
|
84
|
+
Long lines wrap at whitespace boundaries (not mid-word) for English-style text. A single word that's longer than the line — a URL, a class name — still wraps at the character it has to. CJK runs without inter-character whitespace fall back to per-character splitting.
|
|
85
|
+
|
|
65
86
|
### Lists
|
|
66
87
|
|
|
67
88
|
```markdown
|
|
@@ -125,8 +146,41 @@ Uses Rabbit-compatible `{::tag}` notation. Supported size names: `xx-small`, `x-
|
|
|
125
146
|
{::tag name="7"}Maximum size{:/tag}
|
|
126
147
|
```
|
|
127
148
|
|
|
149
|
+
An XML-style alternative is also accepted:
|
|
150
|
+
|
|
151
|
+
```markdown
|
|
152
|
+
<size=x-large>Big text</size>
|
|
153
|
+
<size=7>Maximum size</size>
|
|
154
|
+
```
|
|
155
|
+
|
|
128
156
|
On [Kitty](https://sw.kovidgoyal.net/kitty/)-compatible terminals, sized text is rendered using the OSC 66 text sizing protocol. On other terminals, the markup is silently ignored.
|
|
129
157
|
|
|
158
|
+
### Color
|
|
159
|
+
|
|
160
|
+
Named ANSI colors (`red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, plus `bright_*` variants) and 6-digit hex. Use `{::tag name="..."}` (kramdown form) or the `color` attribute on `<font>` (see [Font](#font)).
|
|
161
|
+
|
|
162
|
+
```markdown
|
|
163
|
+
{::tag name="red"}warning{:/tag}
|
|
164
|
+
{::tag name="ff5555"}custom hex{:/tag}
|
|
165
|
+
|
|
166
|
+
<font color="red">warning</font>
|
|
167
|
+
<font color="ff5555">custom hex</font>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Font
|
|
171
|
+
|
|
172
|
+
HTML 4-style `<font>` tag with `face`, `size`, and `color` attributes. Any subset, in any order. The kramdown shape is also accepted.
|
|
173
|
+
|
|
174
|
+
```markdown
|
|
175
|
+
<font face="Helvetica Neue">Title</font>
|
|
176
|
+
<font face="Menlo" size="3">code</font>
|
|
177
|
+
<font face="Menlo" size="3" color="red">flagged</font>
|
|
178
|
+
|
|
179
|
+
{::font name="Helvetica Neue"}Title{:/font}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`face` requires a terminal that honors the OSC 66 `f=` extension (e.g. [Echoes](https://github.com/amatsuda/echoes)). For PDF export, the family is registered with Prawn via fontconfig — families that can't be found fall through to the default font.
|
|
183
|
+
|
|
130
184
|
### Alignment
|
|
131
185
|
|
|
132
186
|
```markdown
|
|
@@ -137,6 +191,33 @@ centered text
|
|
|
137
191
|
right-aligned text
|
|
138
192
|
```
|
|
139
193
|
|
|
194
|
+
XML form (single-line, paragraph-level):
|
|
195
|
+
|
|
196
|
+
```markdown
|
|
197
|
+
<center>centered <size=3>text</size></center>
|
|
198
|
+
<right>right-aligned</right>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Slide background
|
|
202
|
+
|
|
203
|
+
Set a per-slide background — solid color or linear gradient — via a self-closing block-level directive. Uses the [Echoes](https://github.com/amatsuda/echoes) OSC 7772 extension; other terminals ignore the escape sequence.
|
|
204
|
+
|
|
205
|
+
```markdown
|
|
206
|
+
# Title
|
|
207
|
+
|
|
208
|
+
<bg color="#1a1a2e"/>
|
|
209
|
+
|
|
210
|
+
content...
|
|
211
|
+
|
|
212
|
+
# Second slide
|
|
213
|
+
|
|
214
|
+
<bg from="#1a1a2e" to="#16213e" angle="90"/>
|
|
215
|
+
|
|
216
|
+
content...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The previous slide's background is cleared on every navigation, and on `przn` exit, so your shell isn't left tinted.
|
|
220
|
+
|
|
140
221
|
### Comments
|
|
141
222
|
|
|
142
223
|
```markdown
|
|
@@ -149,8 +230,75 @@ This text is hidden from the presentation.
|
|
|
149
230
|
|
|
150
231
|
```markdown
|
|
151
232
|
Visible text {::note}(speaker note){:/note}
|
|
233
|
+
Visible text <note>(speaker note)</note>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Escaping `<`, `>`, `&`
|
|
237
|
+
|
|
238
|
+
To show literal markup characters that would otherwise be interpreted as a tag, use HTML-style entity references:
|
|
239
|
+
|
|
240
|
+
```markdown
|
|
241
|
+
<note> renders as: <note>
|
|
242
|
+
2 < 3 renders as: 2 < 3
|
|
243
|
+
A & B renders as: A & B
|
|
152
244
|
```
|
|
153
245
|
|
|
246
|
+
A bare `<` not followed by a recognized tag name renders literally as well, so most accidental `<` characters are fine. The entities are only needed when you'd otherwise hit one of the tag patterns (`<size=...>`, `<font ...>`, `<note>`, `<wait/>`, `<center>`, `<right>`, `<bg .../>`).
|
|
247
|
+
|
|
248
|
+
### Wait marker
|
|
249
|
+
|
|
250
|
+
Self-closing presentation flow marker, consumed at parse time:
|
|
251
|
+
|
|
252
|
+
```markdown
|
|
253
|
+
{::wait/}
|
|
254
|
+
<wait/>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Theming
|
|
258
|
+
|
|
259
|
+
Theme resolution:
|
|
260
|
+
|
|
261
|
+
1. **`theme.yml` in the deck's directory** — loaded automatically if present. No flag needed.
|
|
262
|
+
2. **`--theme path/to/your.yml`** — overrides step 1 with any other file you point to.
|
|
263
|
+
3. **`default_theme.yml`** (the file bundled with the gem) — used when neither of the above is found.
|
|
264
|
+
|
|
265
|
+
All keys are optional — anything you don't set falls back to the bundled defaults.
|
|
266
|
+
|
|
267
|
+
```yaml
|
|
268
|
+
font:
|
|
269
|
+
family: # body text font; terminal: OSC 66 f=, PDF: Prawn font
|
|
270
|
+
size: 18 # base PDF font size in pt
|
|
271
|
+
color: # body text color; named ANSI or 6-digit hex
|
|
272
|
+
|
|
273
|
+
title: # h1 typography (slide titles)
|
|
274
|
+
family: # font family
|
|
275
|
+
size: # OSC 66 scale: numeric (1–7) or named (xx-small … xxxx-large); default x-large
|
|
276
|
+
color: # named ANSI or 6-digit hex
|
|
277
|
+
|
|
278
|
+
bullet: # unordered-list marker; also h2–h6 prefix
|
|
279
|
+
text: "・" # the glyph
|
|
280
|
+
size: # OSC 66 scale (1–7) for the bullet; default = body text's scale
|
|
281
|
+
|
|
282
|
+
background: # default slide background (Echoes OSC 7772)
|
|
283
|
+
color: # solid, e.g. "#1a1a2e"
|
|
284
|
+
from: # gradient endpoint
|
|
285
|
+
to: # gradient endpoint
|
|
286
|
+
angle: # gradient angle in degrees
|
|
287
|
+
|
|
288
|
+
colors:
|
|
289
|
+
code_bg: "313244"
|
|
290
|
+
dim: "6c7086"
|
|
291
|
+
inline_code: "a6e3a1"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Notes:
|
|
295
|
+
|
|
296
|
+
- **`font.color`** — deck-wide default text color (terminal: ANSI fg; PDF: Prawn fg). Inline `<color=...>` / `<font color="...">` runs still win per-segment.
|
|
297
|
+
- **`bullet`** — `bullet.text` is the character; `bullet.size` is the OSC 66 scale used to render it. When `bullet.size` is smaller than the body text scale, the bullet is rendered with fractional scaling and vertical centering so it still aligns with the body line.
|
|
298
|
+
- **`font.family`** — applied to body text (terminal: via OSC 66 `f=`, requires Echoes; PDF: registered via fontconfig). Inline `<font face="...">` runs override it per-segment.
|
|
299
|
+
- **`title`** — h1 typography. Each attribute is independent from `font`: `title.family` does **not** inherit `font.family`, `title.color` does **not** inherit `font.color`. `title.size` defaults to x-large (OSC 66 `s=4`). When `title.family` is proportional, every h1 OSC 66 sequence is emitted with `h=2` so a terminal that honors centered horizontal alignment ([Echoes](https://github.com/amatsuda/echoes)) keeps the title visually centered against its reserved cell block. h2–h6 stay body text.
|
|
300
|
+
- **`background`** — the deck-wide default background. A per-slide `<bg .../>` directive overrides it for that slide. The Prawn fallback paints the PDF page in `background.color` when set; otherwise it leaves the page Prawn's default (white).
|
|
301
|
+
|
|
154
302
|
## License
|
|
155
303
|
|
|
156
304
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/default_theme.yml
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
|
-
# Default przn theme
|
|
1
|
+
# Default przn theme. Used when there's neither a `theme.yml` alongside the
|
|
2
|
+
# deck nor a `--theme path/to/file.yml` flag.
|
|
2
3
|
#
|
|
3
|
-
#
|
|
4
|
+
# To customize: drop a `theme.yml` next to your deck (loaded automatically),
|
|
5
|
+
# or pass `--theme path/to/your.yml` to override that.
|
|
4
6
|
# All keys are optional — unspecified values fall back to these defaults.
|
|
5
7
|
|
|
8
|
+
font:
|
|
9
|
+
family: # body text font; terminal: OSC 66 f= (Echoes); PDF: Prawn font (auto-detected if blank)
|
|
10
|
+
size: 18 # base font size in pt for PDF export
|
|
11
|
+
color: # body text color (terminal: ANSI fg, PDF: Prawn fg); named ANSI or 6-digit hex
|
|
12
|
+
|
|
13
|
+
title: # h1 typography (slide titles); each attribute is independent of `font`
|
|
14
|
+
family: # font family (terminal: OSC 66 f=, requires Echoes)
|
|
15
|
+
size: # OSC 66 scale: numeric (1–7) or named (xx-small … xxxx-large); default x-large (4)
|
|
16
|
+
color: # named ANSI or 6-digit hex; falls back to the terminal's default fg when unset
|
|
17
|
+
|
|
18
|
+
bullet: # unordered-list marker; also h2–h6 heading prefix
|
|
19
|
+
text: "・" # the glyph
|
|
20
|
+
size: # OSC 66 scale (1–7) for the bullet; default = body text's scale
|
|
21
|
+
|
|
22
|
+
# Default slide background, applied when a slide has no <bg .../> directive.
|
|
23
|
+
# Echoes-only (OSC 7772). Use either `color` (solid) or `from`/`to`/`angle`
|
|
24
|
+
# (linear gradient). Both unset = no background override.
|
|
25
|
+
background:
|
|
26
|
+
color: # solid color, e.g. "#1a1a2e"
|
|
27
|
+
from: # gradient endpoint, e.g. "#1a1a2e"
|
|
28
|
+
to: # gradient endpoint, e.g. "#16213e"
|
|
29
|
+
angle: # gradient angle in degrees (0 = left→right, 90 = bottom→top)
|
|
30
|
+
type: # gradient type; defaults to "linear"
|
|
31
|
+
|
|
6
32
|
colors:
|
|
7
|
-
background: "000000"
|
|
8
|
-
foreground: "ffffff"
|
|
9
|
-
heading: # falls back to foreground
|
|
10
33
|
code_bg: "313244"
|
|
11
34
|
dim: "6c7086"
|
|
12
35
|
inline_code: "a6e3a1"
|
|
13
|
-
|
|
14
|
-
font:
|
|
15
|
-
family: # auto-detect
|
|
16
|
-
size: 18 # base font size in pt for PDF export
|
data/exe/przn
CHANGED
|
@@ -8,7 +8,7 @@ require 'optparse'
|
|
|
8
8
|
options = {}
|
|
9
9
|
OptionParser.new do |opts|
|
|
10
10
|
opts.banner = "Usage: przn [options] <presentation.md>"
|
|
11
|
-
opts.on('--export [FORMAT]', 'Export to a format (default: pdf)') { |v|
|
|
11
|
+
opts.on('--export [FORMAT]', 'Export to a format (pdf | prawn; default: pdf)') { |v|
|
|
12
12
|
if v && v.end_with?('.md')
|
|
13
13
|
ARGV.unshift(v)
|
|
14
14
|
options[:export] = 'pdf'
|
|
@@ -28,17 +28,33 @@ if options[:generate_theme]
|
|
|
28
28
|
exit
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Extract a leading-`@N` positional like `przn deck.md @42` and treat it as
|
|
32
|
+
# the slide to open the presentation at (1-based). Removed from ARGV so the
|
|
33
|
+
# remaining positional is the deck file.
|
|
34
|
+
start_at = nil
|
|
35
|
+
if (idx = ARGV.index { |a| a =~ /\A@(\d+)\z/ })
|
|
36
|
+
start_at = ARGV.delete_at(idx)[1..].to_i
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
file = ARGV[0]
|
|
32
40
|
unless file
|
|
33
|
-
$stderr.puts "Usage: przn [options] <presentation.md>"
|
|
41
|
+
$stderr.puts "Usage: przn [options] <presentation.md> [@N]"
|
|
34
42
|
exit 1
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
theme =
|
|
45
|
+
theme = if options[:theme]
|
|
46
|
+
Przn::Theme.load(options[:theme])
|
|
47
|
+
else
|
|
48
|
+
Przn::Theme.auto_discover(near: file)
|
|
49
|
+
end
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
case options[:export]
|
|
52
|
+
when 'pdf'
|
|
40
53
|
output = options[:output] || File.basename(file, File.extname(file)) + '.pdf'
|
|
41
54
|
Przn.export_pdf(file, output, theme: theme)
|
|
55
|
+
when 'prawn'
|
|
56
|
+
output = options[:output] || File.basename(file, File.extname(file)) + '.pdf'
|
|
57
|
+
Przn.export_pdf_prawn(file, output, theme: theme)
|
|
42
58
|
else
|
|
43
|
-
Przn.start(file, theme: theme).run
|
|
59
|
+
Przn.start(file, theme: theme, start_at: start_at).run
|
|
44
60
|
end
|
data/lib/przn/controller.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Przn
|
|
|
6
6
|
@presentation = presentation
|
|
7
7
|
@terminal = terminal
|
|
8
8
|
@renderer = renderer
|
|
9
|
+
@preload_gen = 0
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def run
|
|
@@ -34,6 +35,9 @@ module Przn
|
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
ensure
|
|
38
|
+
@preload_gen += 1
|
|
39
|
+
@preload_thread&.join
|
|
40
|
+
@terminal.write "\e]7772;bg-clear\a"
|
|
37
41
|
@terminal.show_cursor
|
|
38
42
|
@terminal.leave_alt_screen
|
|
39
43
|
end
|
|
@@ -46,6 +50,29 @@ module Przn
|
|
|
46
50
|
current: @presentation.current,
|
|
47
51
|
total: @presentation.total
|
|
48
52
|
)
|
|
53
|
+
schedule_preload
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Kick off a background thread that pre-uploads images for the slides the
|
|
57
|
+
# user is most likely to visit next (the immediate neighbors). Uses a
|
|
58
|
+
# generation counter so a navigation that lands while a preload is still
|
|
59
|
+
# running causes that preload to exit early instead of stacking work.
|
|
60
|
+
def schedule_preload
|
|
61
|
+
@preload_gen += 1
|
|
62
|
+
gen = @preload_gen
|
|
63
|
+
cur = @presentation.current
|
|
64
|
+
total = @presentation.total
|
|
65
|
+
indices = [cur + 1, cur - 1].select { |i| i.between?(0, total - 1) }
|
|
66
|
+
return if indices.empty?
|
|
67
|
+
|
|
68
|
+
@preload_thread = Thread.new do
|
|
69
|
+
indices.each do |idx|
|
|
70
|
+
break if gen != @preload_gen
|
|
71
|
+
@renderer.preload(@presentation.slides[idx])
|
|
72
|
+
end
|
|
73
|
+
rescue StandardError
|
|
74
|
+
# Background work must not crash the presentation.
|
|
75
|
+
end
|
|
49
76
|
end
|
|
50
77
|
|
|
51
78
|
def read_key
|
data/lib/przn/image_util.rb
CHANGED
|
@@ -65,6 +65,30 @@ module Przn
|
|
|
65
65
|
ENV['TERM'] == 'xterm-kitty' || ENV['TERM_PROGRAM'] == 'kitty'
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
PNG_MAGIC = "\x89PNG\r\n\x1a\n".b.freeze
|
|
69
|
+
|
|
70
|
+
def png?(path)
|
|
71
|
+
File.open(path, 'rb') { |f| f.read(8)&.b == PNG_MAGIC }
|
|
72
|
+
rescue Errno::ENOENT
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Kitty Graphics Protocol: upload a PNG file by path with the given id.
|
|
77
|
+
# Kitty reads the file directly from disk; we just send a small APC
|
|
78
|
+
# control sequence with the base64-encoded path. Use this once per
|
|
79
|
+
# image; subsequent renders only need a placement command.
|
|
80
|
+
# https://sw.kovidgoyal.net/kitty/graphics-protocol/
|
|
81
|
+
def kitty_upload_png(path, image_id:)
|
|
82
|
+
encoded = [File.expand_path(path)].pack('m0')
|
|
83
|
+
"\e_Ga=t,t=f,f=100,i=#{image_id},q=2;#{encoded}\e\\"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Kitty Graphics Protocol: place a previously-uploaded image at the
|
|
87
|
+
# current cursor position, scaled to fit `cols` x `rows` cells.
|
|
88
|
+
def kitty_place(image_id:, cols:, rows:)
|
|
89
|
+
"\e_Ga=p,i=#{image_id},c=#{cols},r=#{rows},q=2\e\\"
|
|
90
|
+
end
|
|
91
|
+
|
|
68
92
|
# Sixel via img2sixel
|
|
69
93
|
def sixel_available?
|
|
70
94
|
@sixel_available = system('command -v img2sixel > /dev/null 2>&1') if @sixel_available.nil?
|
data/lib/przn/kitty_text.rb
CHANGED
|
@@ -10,10 +10,13 @@ module Przn
|
|
|
10
10
|
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
-
def sized(text, s:, h: nil, v: nil)
|
|
13
|
+
def sized(text, s:, h: nil, v: nil, n: nil, d: nil, f: nil)
|
|
14
14
|
params = +"s=#{s}"
|
|
15
|
+
params << ":n=#{n}" if n
|
|
16
|
+
params << ":d=#{d}" if d
|
|
15
17
|
params << ":h=#{h}" if h
|
|
16
18
|
params << ":v=#{v}" if v
|
|
19
|
+
params << ":f=#{f}" if f
|
|
17
20
|
"\e]66;#{params};#{text}\a"
|
|
18
21
|
end
|
|
19
22
|
|
data/lib/przn/parser.rb
CHANGED
|
@@ -71,6 +71,17 @@ module Przn
|
|
|
71
71
|
when /\A\s*\{:\.(\w+)\}\s*\z/
|
|
72
72
|
blocks << {type: :align, align: Regexp.last_match(1).to_sym}
|
|
73
73
|
|
|
74
|
+
# Block alignment, XML form: <center>content</center> or <right>content</right>
|
|
75
|
+
when /\A\s*<(center|right)>(.*)<\/\1>\s*\z/
|
|
76
|
+
blocks << {type: :align, align: Regexp.last_match(1).to_sym}
|
|
77
|
+
blocks << {type: :paragraph, content: Regexp.last_match(2)}
|
|
78
|
+
|
|
79
|
+
# Slide background (Echoes OSC 7772):
|
|
80
|
+
# <bg color="#..."/> — solid (bg-color)
|
|
81
|
+
# <bg from="#..." to="#..." angle="N"/> — linear gradient (bg-gradient)
|
|
82
|
+
when /\A\s*<bg((?:\s+\w+="[^"]+")*)\s*\/>\s*\z/
|
|
83
|
+
blocks << {type: :bg, attrs: parse_xml_attrs(Regexp.last_match(1))}
|
|
84
|
+
|
|
74
85
|
# Fenced code block
|
|
75
86
|
when /\A\s*```(\w*)\s*\z/
|
|
76
87
|
lang = Regexp.last_match(1)
|
|
@@ -217,6 +228,25 @@ module Przn
|
|
|
217
228
|
Slide.new(blocks)
|
|
218
229
|
end
|
|
219
230
|
|
|
231
|
+
# HTML4-style <font face="..." size="..." color="..."> attributes.
|
|
232
|
+
# Kramdown's {::font name="..."} legacy spelling for the family is also
|
|
233
|
+
# accepted and folded into :face so the renderer has one shape to handle.
|
|
234
|
+
def parse_font_attrs(str)
|
|
235
|
+
attrs = parse_xml_attrs(str)
|
|
236
|
+
attrs[:face] = attrs.delete(:name) if attrs.key?(:name) && !attrs.key?(:face)
|
|
237
|
+
attrs.slice(:face, :size, :color)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Generic attribute scanner — `key="value"` pairs, returned as a hash with
|
|
241
|
+
# symbolized keys. Doesn't validate which keys are allowed; callers slice.
|
|
242
|
+
def parse_xml_attrs(str)
|
|
243
|
+
attrs = {}
|
|
244
|
+
str.scan(/(\w+)="([^"]+)"/) do |key, value|
|
|
245
|
+
attrs[key.to_sym] = value
|
|
246
|
+
end
|
|
247
|
+
attrs
|
|
248
|
+
end
|
|
249
|
+
|
|
220
250
|
def parse_image_attrs(str, attrs)
|
|
221
251
|
str = str.sub(/\A:?\s*/, '')
|
|
222
252
|
str.scan(/([\w-]+)=['"]([^'"]*)['"]/) do |key, value|
|
|
@@ -243,10 +273,24 @@ module Przn
|
|
|
243
273
|
until scanner.eos?
|
|
244
274
|
if scanner.scan(/\{::tag\s+name="([^"]+)"\}(.*?)\{:\/tag\}/)
|
|
245
275
|
segments << [:tag, scanner[2], scanner[1]]
|
|
276
|
+
elsif scanner.scan(/<size=([^>\s]+)>(.*?)<\/size>/)
|
|
277
|
+
segments << [:tag, scanner[2], scanner[1]]
|
|
278
|
+
elsif scanner.scan(/<font((?:\s+\w+="[^"]+")+)\s*>(.*?)<\/font>/)
|
|
279
|
+
segments << [:font, scanner[2], parse_font_attrs(scanner[1])]
|
|
280
|
+
elsif scanner.scan(/\{::font((?:\s+\w+="[^"]+")+)\}(.*?)\{:\/font\}/)
|
|
281
|
+
segments << [:font, scanner[2], parse_font_attrs(scanner[1])]
|
|
246
282
|
elsif scanner.scan(/\{::note\}(.*?)\{:\/note\}/)
|
|
247
283
|
segments << [:note, scanner[1]]
|
|
248
|
-
elsif scanner.scan(
|
|
284
|
+
elsif scanner.scan(/<note>(.*?)<\/note>/)
|
|
285
|
+
segments << [:note, scanner[1]]
|
|
286
|
+
elsif scanner.scan(/\{::wait\/\}/) || scanner.scan(/<wait\s*\/>/)
|
|
249
287
|
# skip wait markers in inline text
|
|
288
|
+
elsif scanner.scan(/</)
|
|
289
|
+
segments << [:text, "<"]
|
|
290
|
+
elsif scanner.scan(/>/)
|
|
291
|
+
segments << [:text, ">"]
|
|
292
|
+
elsif scanner.scan(/&/)
|
|
293
|
+
segments << [:text, "&"]
|
|
250
294
|
elsif scanner.scan(/`([^`]+)`/)
|
|
251
295
|
segments << [:code, scanner[1]]
|
|
252
296
|
elsif scanner.scan(/\*\*(.+?)\*\*/)
|
|
@@ -256,7 +300,7 @@ module Przn
|
|
|
256
300
|
elsif scanner.scan(/~~(.+?)~~/)
|
|
257
301
|
segments << [:strikethrough, scanner[1]]
|
|
258
302
|
else
|
|
259
|
-
segments << [:text, scanner.scan(/[^`*~{]+|./)]
|
|
303
|
+
segments << [:text, scanner.scan(/[^`*~{<&]+|./)]
|
|
260
304
|
end
|
|
261
305
|
end
|
|
262
306
|
|
data/lib/przn/pdf_exporter.rb
CHANGED
|
@@ -55,12 +55,11 @@ module Przn
|
|
|
55
55
|
@presentation = presentation
|
|
56
56
|
@base_dir = base_dir
|
|
57
57
|
@theme = theme || Theme.default
|
|
58
|
-
@bg_color = @theme.
|
|
59
|
-
@fg_color = @theme.
|
|
58
|
+
@bg_color = @theme.background && @theme.background[:color]
|
|
59
|
+
@fg_color = @theme.font[:color] || "000000"
|
|
60
60
|
@code_bg = @theme.colors[:code_bg]
|
|
61
61
|
@dim_color = @theme.colors[:dim]
|
|
62
62
|
@inline_code_color = @theme.colors[:inline_code]
|
|
63
|
-
@heading_color = @theme.colors[:heading] || @fg_color
|
|
64
63
|
base = (@theme.font[:size] || DEFAULT_FONT_SIZE).to_f
|
|
65
64
|
ratio = base / DEFAULT_FONT_SIZE
|
|
66
65
|
@scale_to_pt = DEFAULT_SCALE_TO_PT.transform_values { |v| v * ratio }
|
|
@@ -127,6 +126,57 @@ module Przn
|
|
|
127
126
|
@font_registered = true
|
|
128
127
|
|
|
129
128
|
register_emoji_fallback(pdf, font_path)
|
|
129
|
+
register_inline_fonts(pdf)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Pre-register every font family referenced by an inline <font face="..."> /
|
|
133
|
+
# {::font name="..."} tag, so build_formatted_text can use it as a Prawn
|
|
134
|
+
# font: option. Families that fontconfig can't locate are silently dropped
|
|
135
|
+
# — those runs render in the default font instead of erroring.
|
|
136
|
+
def register_inline_fonts(pdf)
|
|
137
|
+
@registered_inline_fonts = {}
|
|
138
|
+
inline_font_families.each do |family|
|
|
139
|
+
next if @registered_inline_fonts.key?(family) || family == 'CJK'
|
|
140
|
+
|
|
141
|
+
path = fc_find(family)
|
|
142
|
+
next unless path
|
|
143
|
+
bold_path = find_bold_font(path, family)
|
|
144
|
+
pdf.font_families.update(family => {
|
|
145
|
+
normal: path,
|
|
146
|
+
bold: bold_path,
|
|
147
|
+
italic: path,
|
|
148
|
+
})
|
|
149
|
+
@registered_inline_fonts[family] = true
|
|
150
|
+
rescue
|
|
151
|
+
next
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def inline_font_families
|
|
156
|
+
families = []
|
|
157
|
+
@presentation.slides.each do |slide|
|
|
158
|
+
slide.blocks.each do |b|
|
|
159
|
+
texts = []
|
|
160
|
+
texts << b[:content] if b[:content].is_a?(String)
|
|
161
|
+
texts << b[:term] if b[:term].is_a?(String)
|
|
162
|
+
texts << b[:definition] if b[:definition].is_a?(String)
|
|
163
|
+
if b[:items].is_a?(Array)
|
|
164
|
+
b[:items].each { |it| texts << it[:text] if it[:text].is_a?(String) }
|
|
165
|
+
end
|
|
166
|
+
if b[:rows].is_a?(Array)
|
|
167
|
+
(Array(b[:header]) + b[:rows].flatten).each { |c| texts << c if c.is_a?(String) }
|
|
168
|
+
end
|
|
169
|
+
texts.each do |text|
|
|
170
|
+
text.scan(/<font((?:\s+\w+="[^"]+")+)\s*>/) do
|
|
171
|
+
families << Parser.parse_font_attrs(Regexp.last_match(1))[:face]
|
|
172
|
+
end
|
|
173
|
+
text.scan(/\{::font((?:\s+\w+="[^"]+")+)\}/) do
|
|
174
|
+
families << Parser.parse_font_attrs(Regexp.last_match(1))[:face]
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
families.compact.uniq
|
|
130
180
|
end
|
|
131
181
|
|
|
132
182
|
def find_font
|
|
@@ -237,6 +287,8 @@ module Przn
|
|
|
237
287
|
end
|
|
238
288
|
|
|
239
289
|
def draw_background(pdf)
|
|
290
|
+
return unless @bg_color
|
|
291
|
+
|
|
240
292
|
pdf.canvas do
|
|
241
293
|
pdf.fill_color @bg_color
|
|
242
294
|
pdf.fill_rectangle [0, PAGE_HEIGHT], PAGE_WIDTH, PAGE_HEIGHT
|
|
@@ -270,7 +322,7 @@ module Przn
|
|
|
270
322
|
y - h - heading_margin(pt)
|
|
271
323
|
else
|
|
272
324
|
pt = @scale_to_pt[DEFAULT_SCALE]
|
|
273
|
-
prefix = [{text: bullet, size: pt, color: @
|
|
325
|
+
prefix = [{text: bullet, size: pt, color: @fg_color, styles: [:bold]}]
|
|
274
326
|
formatted = prefix + build_formatted_text(text, pt)
|
|
275
327
|
h = render_formatted(pdf, formatted, at: [margin_x, y], width: content_width)
|
|
276
328
|
y - h - 4
|
|
@@ -455,6 +507,25 @@ module Przn
|
|
|
455
507
|
else
|
|
456
508
|
{text: content, size: default_pt, color: @fg_color}
|
|
457
509
|
end
|
|
510
|
+
when :font
|
|
511
|
+
attrs = segment[2].is_a?(Hash) ? segment[2] : {}
|
|
512
|
+
run = {text: content, size: default_pt, color: @fg_color}
|
|
513
|
+
if attrs[:face] && @registered_inline_fonts&.key?(attrs[:face])
|
|
514
|
+
run[:font] = attrs[:face]
|
|
515
|
+
end
|
|
516
|
+
if attrs[:size] && (scale = Parser::SIZE_SCALES[attrs[:size]])
|
|
517
|
+
run[:size] = @scale_to_pt[scale]
|
|
518
|
+
end
|
|
519
|
+
if attrs[:color]
|
|
520
|
+
run[:color] = if (hex = COLOR_MAP[attrs[:color]])
|
|
521
|
+
hex
|
|
522
|
+
elsif attrs[:color].match?(/\A[0-9a-fA-F]{6}\z/)
|
|
523
|
+
attrs[:color].upcase
|
|
524
|
+
else
|
|
525
|
+
@fg_color
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
run
|
|
458
529
|
when :bold
|
|
459
530
|
{text: content, size: default_pt, color: @fg_color, styles: [:bold]}
|
|
460
531
|
when :italic
|
|
@@ -538,7 +609,7 @@ module Przn
|
|
|
538
609
|
end
|
|
539
610
|
|
|
540
611
|
def bullet
|
|
541
|
-
@font_registered ?
|
|
612
|
+
@font_registered ? @theme.bullet[:text] : "-"
|
|
542
613
|
end
|
|
543
614
|
|
|
544
615
|
end
|