przn 0.4.0 → 0.5.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 +45 -0
- data/lib/przn/controller.rb +1 -0
- data/lib/przn/image_util.rb +10 -0
- data/lib/przn/parser.rb +78 -8
- data/lib/przn/renderer.rb +77 -10
- data/lib/przn/screenshot_pdf_exporter.rb +1 -0
- data/lib/przn/version.rb +1 -1
- data/lib/przn.rb +1 -0
- data/sample/sample.md +16 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 926f4301117fde5642b907fbf96340d1e547af78086eb1fd47ed45fa16472e5e
|
|
4
|
+
data.tar.gz: 8cbbf68f1d784b6c6ed2ce7ba600c0a214dc8a688a785b5044701b9e8c587b39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f6a58582c7a21ccfab9750ee0ebc39525626af2233d503497e933a1d5b108bcf08b34c156115730c145e3c4dd7473b8d48633597d68b487bcc5702e29132183
|
|
7
|
+
data.tar.gz: 0a3bf226c4c4e6300d515cf50274c1ad266556c9a984df6090ed9dec1dbcc7cc3e878675131e2d26f0e71b1167046f1d5ea3e7f1b0f965101f942e5a728d9967
|
data/README.md
CHANGED
|
@@ -75,6 +75,8 @@ przn --export prawn -o output.pdf your_slides.md
|
|
|
75
75
|
|
|
76
76
|
przn's Markdown format is compatible with [Rabbit](https://rabbit-shocker.org/)'s Markdown mode.
|
|
77
77
|
|
|
78
|
+
> **HTML-ish tag attributes** — every `<tag attr=value>` block below (`<bg>`, `<at>`, `<img>`, `<font>`) accepts three value forms: double-quoted `attr="value"`, single-quoted `attr='value'`, and unquoted `attr=value` (HTML5-ish — anything that isn't whitespace, `=`, `<`, `>`, a quote, or backtick). Self-closing tags need a space before `/>` when the last attribute is unquoted (`<img src=foo.png />`).
|
|
79
|
+
|
|
78
80
|
### Slide splitting
|
|
79
81
|
|
|
80
82
|
Slides are separated by `#` (h1) headings.
|
|
@@ -235,6 +237,49 @@ content...
|
|
|
235
237
|
|
|
236
238
|
The previous slide's background is cleared on every navigation, and on `przn` exit, so your shell isn't left tinted.
|
|
237
239
|
|
|
240
|
+
### Absolute-position text
|
|
241
|
+
|
|
242
|
+
Place text at an arbitrary `(column, row)` on the slide, escaping the normal top-down paragraph flow:
|
|
243
|
+
|
|
244
|
+
```markdown
|
|
245
|
+
# Layout test
|
|
246
|
+
|
|
247
|
+
<at x="10" y="5">top-left ish</at>
|
|
248
|
+
<at x="40" y="15"><size=3>BIG</size></at>
|
|
249
|
+
<at x="80" y="25"><color=red>warn</color></at>
|
|
250
|
+
<at x="50%" y="50%">dead center</at>
|
|
251
|
+
|
|
252
|
+
{::at x="10" y="20"}same thing, kramdown form{:/at}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
- `x` / `y` accept two forms:
|
|
256
|
+
- **Plain integer** — 1-based terminal cells, matching the cursor-position escape (`\e[y;xH`). `x="1" y="1"` is the very top-left of the slide pane.
|
|
257
|
+
- **Percent** (`x="50%"`, `y="100%"`) — resolves against the terminal's current width / height. Auto-adjusts when the pane is resized.
|
|
258
|
+
- Content is parsed inline, so all the usual styling works inside an `<at>` — `<size>`, `<color>`, `<font>`, `**bold**`, `*italic*`, etc.
|
|
259
|
+
- The block doesn't take up vertical space in the slide's layout — paragraphs around it render in their normal positions and the absolute placement layers on top. Useful for overlaying labels on a `<bg .../>` gradient or pinning annotations to specific cells.
|
|
260
|
+
- Out-of-range coordinates clamp into the visible area; missing / unparseable coordinates skip silently.
|
|
261
|
+
|
|
262
|
+
### Image
|
|
263
|
+
|
|
264
|
+
Embed an image with the standard markdown form, or the `<img>` XML form when you want to absolute-position it. Both produce identical output — `<img>` just opens the door to extra attributes like `x` / `y`.
|
|
265
|
+
|
|
266
|
+
```markdown
|
|
267
|
+
{:relative_height="70"}
|
|
268
|
+
<img src="doge.png" relative_height="70"/>
|
|
269
|
+
|
|
270
|
+
<img src="doge.png" x="5" y="3" relative_height="40"/>
|
|
271
|
+
<img src="doge.png" x="50%" y="50%" relative_height="40"/>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- `src` is required; `alt` and `title` are accepted and ignored at render time (kept for accessibility / future use).
|
|
275
|
+
- `relative_height="N"` caps the image at N % of the terminal height (default 70). Aspect ratio is preserved. `relative_width="N"` is the same for the horizontal dimension.
|
|
276
|
+
- `height="N%"` / `width="N%"` are short-form aliases for `relative_height` / `relative_width` (both forms — `<img>` and `![]{:...}` — accept the alias). An explicit `relative_*` on the same block wins; a non-`%` value (`height="40"`) is left alone since pixel units aren't supported.
|
|
277
|
+
- `x` / `y` (optional) anchor the image's top-left at an absolute cell. Same two forms as [`<at>`](#absolute-position-text):
|
|
278
|
+
- **Plain integer** — 1-based terminal cells.
|
|
279
|
+
- **Percent** — resolves against the terminal's current width / height.
|
|
280
|
+
- With `x` and `y` set, the image layers on top of the slide and contributes 0 to the layout flow — paragraphs around it render in their normal positions, exactly like `<at>`. Without `x` / `y`, the image stays horizontally centered and takes up its natural height in the flow.
|
|
281
|
+
- Rendering backend: Kitty Graphics Protocol on terminals that support it (PNG uploaded once and reused; JPG goes through `kitten icat`), Sixel as a fallback. Other terminals show nothing in place of the image.
|
|
282
|
+
|
|
238
283
|
### Comments
|
|
239
284
|
|
|
240
285
|
```markdown
|
data/lib/przn/controller.rb
CHANGED
data/lib/przn/image_util.rb
CHANGED
|
@@ -91,6 +91,16 @@ module Przn
|
|
|
91
91
|
"\e_Ga=p,i=#{image_id},c=#{cols},r=#{rows},q=2\e\\"
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
# Kitty Graphics Protocol: delete every placement and free the
|
|
95
|
+
# stored image data. Used on quit so previously-rendered images
|
|
96
|
+
# don't leak through onto the user's restored shell screen
|
|
97
|
+
# (placements aren't tied to the alt-screen buffer in most
|
|
98
|
+
# kitty-protocol implementations, so leaving the alt screen
|
|
99
|
+
# alone isn't enough to hide them). `q=2` suppresses the OK reply.
|
|
100
|
+
def kitty_clear_all
|
|
101
|
+
"\e_Ga=d,d=A,q=2\e\\"
|
|
102
|
+
end
|
|
103
|
+
|
|
94
104
|
# Sixel via img2sixel
|
|
95
105
|
def sixel_available?
|
|
96
106
|
@sixel_available = system('command -v img2sixel > /dev/null 2>&1') if @sixel_available.nil?
|
data/lib/przn/parser.rb
CHANGED
|
@@ -25,6 +25,15 @@ module Przn
|
|
|
25
25
|
'bright_white' => 97
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
28
|
+
# HTML-ish attribute, three accepted value forms:
|
|
29
|
+
# key="value" key='value' key=bareword
|
|
30
|
+
# The unquoted token excludes whitespace, `=`, `<`, `>`, `"`, `'` and
|
|
31
|
+
# backtick, matching the spirit of HTML5's unquoted-attribute grammar.
|
|
32
|
+
# `/` is intentionally NOT excluded so paths like `src=path/to/file`
|
|
33
|
+
# work — which means self-closing tags need a space before `/>` when
|
|
34
|
+
# the last attribute is unquoted (`<img src=foo.png />`).
|
|
35
|
+
ATTR_RE_SRC = '\w+=(?:"[^"]*"|\'[^\']*\'|[^\s=<>"\'`]+)'
|
|
36
|
+
|
|
28
37
|
module_function
|
|
29
38
|
|
|
30
39
|
def parse(markdown)
|
|
@@ -79,9 +88,20 @@ module Przn
|
|
|
79
88
|
# Slide background (Echoes OSC 7772):
|
|
80
89
|
# <bg color="#..."/> — solid (bg-color)
|
|
81
90
|
# <bg from="#..." to="#..." angle="N"/> — linear gradient (bg-gradient)
|
|
82
|
-
|
|
91
|
+
# Attribute values may be double-quoted, single-quoted, or
|
|
92
|
+
# unquoted (HTML5-ish — see ATTR_RE_SRC).
|
|
93
|
+
when %r{\A\s*<bg((?:\s+#{ATTR_RE_SRC})*)\s*/>\s*\z}o
|
|
83
94
|
blocks << {type: :bg, attrs: parse_xml_attrs(Regexp.last_match(1))}
|
|
84
95
|
|
|
96
|
+
# Absolute-position text:
|
|
97
|
+
# <at x="N" y="N">content</at>
|
|
98
|
+
# {::at x="N" y="N"}content{:/at}
|
|
99
|
+
# Content can include inline markup (size, color, font, bold, …).
|
|
100
|
+
when %r{\A\s*<at((?:\s+#{ATTR_RE_SRC})+)\s*>(.*)</at>\s*\z}o
|
|
101
|
+
blocks << {type: :at, attrs: parse_xml_attrs(Regexp.last_match(1)), content: Regexp.last_match(2)}
|
|
102
|
+
when %r{\A\s*\{::at((?:\s+#{ATTR_RE_SRC})+)\}(.*)\{:/at\}\s*\z}o
|
|
103
|
+
blocks << {type: :at, attrs: parse_xml_attrs(Regexp.last_match(1)), content: Regexp.last_match(2)}
|
|
104
|
+
|
|
85
105
|
# Fenced code block
|
|
86
106
|
when /\A\s*```(\w*)\s*\z/
|
|
87
107
|
lang = Regexp.last_match(1)
|
|
@@ -176,6 +196,23 @@ module Przn
|
|
|
176
196
|
i -= 1
|
|
177
197
|
blocks << {type: :ordered_list, items: items}
|
|
178
198
|
|
|
199
|
+
# Image, XML form: <img src="path" alt="..." title="..." {:attrs}/>
|
|
200
|
+
# Equivalent to the markdown `{:attrs}` form below
|
|
201
|
+
# — emits the same `:image` block so the renderer handles both
|
|
202
|
+
# identically. `src` is required; all other attributes pass through
|
|
203
|
+
# to `block[:attrs]` (string-keyed, matching markdown's IAL parse) so
|
|
204
|
+
# `relative_height`, `width`, etc. work the same way.
|
|
205
|
+
when %r{\A\s*<img((?:\s+#{ATTR_RE_SRC})+)\s*/>\s*\z}o
|
|
206
|
+
raw = parse_xml_attrs(Regexp.last_match(1))
|
|
207
|
+
path = raw.delete(:src)
|
|
208
|
+
if path
|
|
209
|
+
alt = raw.delete(:alt).to_s
|
|
210
|
+
title = raw.delete(:title)
|
|
211
|
+
attrs = raw.transform_keys(&:to_s)
|
|
212
|
+
normalize_image_attrs!(attrs)
|
|
213
|
+
blocks << {type: :image, path: path, alt: alt, title: title, attrs: attrs}
|
|
214
|
+
end
|
|
215
|
+
|
|
179
216
|
# Image: {:attrs}
|
|
180
217
|
when /\A!\[([^\]]*)\]\((\S+?)(?:\s+"([^"]*)")?\)(.*)/
|
|
181
218
|
alt = Regexp.last_match(1)
|
|
@@ -194,6 +231,7 @@ module Przn
|
|
|
194
231
|
attr_str = attr_str.sub(/\}\s*\z/, '')
|
|
195
232
|
parse_image_attrs(attr_str, attrs)
|
|
196
233
|
end
|
|
234
|
+
normalize_image_attrs!(attrs)
|
|
197
235
|
blocks << {type: :image, path: path, alt: alt, title: title, attrs: attrs}
|
|
198
236
|
|
|
199
237
|
# Definition list: term on one line, : definition on next
|
|
@@ -237,12 +275,14 @@ module Przn
|
|
|
237
275
|
attrs.slice(:face, :size, :color)
|
|
238
276
|
end
|
|
239
277
|
|
|
240
|
-
# Generic attribute scanner —
|
|
241
|
-
#
|
|
278
|
+
# Generic attribute scanner — three value forms accepted:
|
|
279
|
+
# key="value" key='value' key=bareword
|
|
280
|
+
# Returns a hash with symbolized keys. Doesn't validate which keys
|
|
281
|
+
# are allowed; callers slice.
|
|
242
282
|
def parse_xml_attrs(str)
|
|
243
283
|
attrs = {}
|
|
244
|
-
str.scan(/(\w+)="([^"]+)
|
|
245
|
-
attrs[key.to_sym] =
|
|
284
|
+
str.scan(/(\w+)=(?:"([^"]*)"|'([^']*)'|([^\s=<>"'`]+))/) do |key, dq, sq, uq|
|
|
285
|
+
attrs[key.to_sym] = dq || sq || uq
|
|
246
286
|
end
|
|
247
287
|
attrs
|
|
248
288
|
end
|
|
@@ -254,6 +294,22 @@ module Przn
|
|
|
254
294
|
end
|
|
255
295
|
end
|
|
256
296
|
|
|
297
|
+
# Rewrite `height="N%"` / `width="N%"` into the canonical
|
|
298
|
+
# `relative_height="N"` / `relative_width="N"` the renderer reads.
|
|
299
|
+
# Values without a `%` suffix pass through unchanged (and are
|
|
300
|
+
# ignored downstream); an explicit `relative_*` already on the
|
|
301
|
+
# block wins so authors can mix forms without surprise.
|
|
302
|
+
def normalize_image_attrs!(attrs)
|
|
303
|
+
if (h = attrs['height']) && (m = h.match(/\A(\d+)%\z/))
|
|
304
|
+
attrs.delete('height')
|
|
305
|
+
attrs['relative_height'] ||= m[1]
|
|
306
|
+
end
|
|
307
|
+
if (w = attrs['width']) && (m = w.match(/\A(\d+)%\z/))
|
|
308
|
+
attrs.delete('width')
|
|
309
|
+
attrs['relative_width'] ||= m[1]
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
257
313
|
def parse_table(lines)
|
|
258
314
|
rows = []
|
|
259
315
|
lines.each do |line|
|
|
@@ -275,9 +331,9 @@ module Przn
|
|
|
275
331
|
segments << [:tag, scanner[2], scanner[1]]
|
|
276
332
|
elsif scanner.scan(/<size=([^>\s]+)>(.*?)<\/size>/)
|
|
277
333
|
segments << [:tag, scanner[2], scanner[1]]
|
|
278
|
-
elsif scanner.scan(
|
|
334
|
+
elsif scanner.scan(%r{<font((?:\s+#{ATTR_RE_SRC})+)\s*>(.*?)</font>}o)
|
|
279
335
|
segments << [:font, scanner[2], parse_font_attrs(scanner[1])]
|
|
280
|
-
elsif scanner.scan(
|
|
336
|
+
elsif scanner.scan(%r{\{::font((?:\s+#{ATTR_RE_SRC})+)\}(.*?)\{:/font\}}o)
|
|
281
337
|
segments << [:font, scanner[2], parse_font_attrs(scanner[1])]
|
|
282
338
|
elsif scanner.scan(/\{::note\}(.*?)\{:\/note\}/)
|
|
283
339
|
segments << [:note, scanner[1]]
|
|
@@ -304,7 +360,21 @@ module Przn
|
|
|
304
360
|
end
|
|
305
361
|
end
|
|
306
362
|
|
|
307
|
-
segments
|
|
363
|
+
# Coalesce adjacent :text segments. The scanner has to bail to a
|
|
364
|
+
# single-character `.` when it sees `&` so the `<` / `>` /
|
|
365
|
+
# `&` entity matches can run on the next iteration, which
|
|
366
|
+
# leaves a bare `&` as its own segment and fragments the
|
|
367
|
+
# surrounding text. Merging them back together means one OSC 66
|
|
368
|
+
# multicell sequence per typeset run — important for h1 titles
|
|
369
|
+
# under a proportional font, where Echoes pads each run
|
|
370
|
+
# independently and stray segments become visible gaps.
|
|
371
|
+
segments.each_with_object([]) do |seg, acc|
|
|
372
|
+
if seg[0] == :text && acc.last && acc.last[0] == :text
|
|
373
|
+
acc.last[1] = acc.last[1] + seg[1]
|
|
374
|
+
else
|
|
375
|
+
acc << seg
|
|
376
|
+
end
|
|
377
|
+
end
|
|
308
378
|
end
|
|
309
379
|
end
|
|
310
380
|
end
|
data/lib/przn/renderer.rb
CHANGED
|
@@ -109,6 +109,7 @@ module Przn
|
|
|
109
109
|
when :image then render_image(block, width, row)
|
|
110
110
|
when :blank then row + DEFAULT_SCALE
|
|
111
111
|
when :bg then row
|
|
112
|
+
when :at then render_at(block); row
|
|
112
113
|
else row + 1
|
|
113
114
|
end
|
|
114
115
|
end
|
|
@@ -137,6 +138,48 @@ module Przn
|
|
|
137
138
|
@terminal.write "\e]7772;bg-gradient;type=#{type}:angle=#{angle}:colors=#{colors.join(',')}\a"
|
|
138
139
|
end
|
|
139
140
|
|
|
141
|
+
# Place text at an absolute (column, row) on the slide, escaping the
|
|
142
|
+
# normal top-down paragraph flow. Coordinates are 1-based terminal cells
|
|
143
|
+
# to match the CSI cursor-position escape. A trailing `%` interprets the
|
|
144
|
+
# value as a percentage of the terminal's width (for `x=`) or height
|
|
145
|
+
# (for `y=`) — `x="50%" y="50%"` lands at the middle of the pane,
|
|
146
|
+
# auto-resizing with the terminal. Content is parsed inline so
|
|
147
|
+
# `<size>`, `<color>`, `<font>`, **bold**, etc. all work inside `<at>`.
|
|
148
|
+
# The block contributes 0 to the slide's layout height so it doesn't
|
|
149
|
+
# push subsequent content down.
|
|
150
|
+
def render_at(block)
|
|
151
|
+
attrs = block[:attrs] || {}
|
|
152
|
+
x = resolve_at_coord(attrs[:x], @terminal.width)
|
|
153
|
+
y = resolve_at_coord(attrs[:y], @terminal.height)
|
|
154
|
+
return if x.nil? || y.nil?
|
|
155
|
+
|
|
156
|
+
segments = Parser.parse_inline(block[:content].to_s)
|
|
157
|
+
@terminal.move_to(y, x)
|
|
158
|
+
@terminal.write render_segments_scaled(segments, DEFAULT_SCALE)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Resolve an `<at>` coordinate string against the dimension it indexes.
|
|
162
|
+
# `"50%"` → halfway along `max`; plain integer string → that number of
|
|
163
|
+
# cells. Out-of-range values clamp into [1, max]. Returns nil when the
|
|
164
|
+
# input is missing or unparseable so the renderer skips silently.
|
|
165
|
+
def resolve_at_coord(raw, max)
|
|
166
|
+
return nil if raw.nil?
|
|
167
|
+
|
|
168
|
+
s = raw.to_s.strip
|
|
169
|
+
return nil if s.empty?
|
|
170
|
+
|
|
171
|
+
cells =
|
|
172
|
+
if s.end_with?('%')
|
|
173
|
+
pct = s.chomp('%').to_f
|
|
174
|
+
(pct / 100.0 * max).round
|
|
175
|
+
elsif s =~ /\A-?\d+\z/
|
|
176
|
+
s.to_i
|
|
177
|
+
end
|
|
178
|
+
return nil if cells.nil?
|
|
179
|
+
|
|
180
|
+
cells.clamp(1, max)
|
|
181
|
+
end
|
|
182
|
+
|
|
140
183
|
# Bottom-row progress indicator (Rabbit-style):
|
|
141
184
|
#
|
|
142
185
|
# 1 🐢 🐇 9
|
|
@@ -415,9 +458,19 @@ module Przn
|
|
|
415
458
|
img_w, img_h = img_size
|
|
416
459
|
cell_w, cell_h = @terminal.cell_pixel_size
|
|
417
460
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
461
|
+
attrs = block[:attrs] || {}
|
|
462
|
+
abs_x = resolve_at_coord(attrs['x'], @terminal.width)
|
|
463
|
+
abs_y = resolve_at_coord(attrs['y'], @terminal.height)
|
|
464
|
+
absolute = abs_x && abs_y
|
|
465
|
+
|
|
466
|
+
origin_row = absolute ? abs_y : row
|
|
467
|
+
available_rows = [@terminal.height - origin_row - 2, 1].max
|
|
468
|
+
if absolute
|
|
469
|
+
available_cols = [@terminal.width - abs_x + 1, 1].max
|
|
470
|
+
else
|
|
471
|
+
left = content_left(width)
|
|
472
|
+
available_cols = width - left * 2
|
|
473
|
+
end
|
|
421
474
|
|
|
422
475
|
# Cap the default vertical area to 70 % of the screen, matching what
|
|
423
476
|
# `{:relative_height="70"}` would do explicitly. Large images that
|
|
@@ -426,7 +479,7 @@ module Przn
|
|
|
426
479
|
# smaller images sit well within this cap so they're unaffected.
|
|
427
480
|
# An explicit `relative_height` still overrides.
|
|
428
481
|
default_rh = DEFAULT_IMAGE_RELATIVE_HEIGHT_PERCENT
|
|
429
|
-
rh =
|
|
482
|
+
rh = attrs['relative_height'] || default_rh
|
|
430
483
|
target_rows = (@terminal.height * rh.to_i / 100.0).to_i
|
|
431
484
|
available_rows = [target_rows, available_rows].min
|
|
432
485
|
|
|
@@ -439,24 +492,29 @@ module Przn
|
|
|
439
492
|
target_cols = [target_cols, 1].max
|
|
440
493
|
target_rows = [target_rows, 1].max
|
|
441
494
|
|
|
442
|
-
|
|
495
|
+
if absolute
|
|
496
|
+
y_cell, x_cell = abs_y, abs_x
|
|
497
|
+
else
|
|
498
|
+
y_cell = row
|
|
499
|
+
x_cell = [(width - target_cols) / 2, 0].max + 1
|
|
500
|
+
end
|
|
443
501
|
|
|
444
502
|
if ImageUtil.kitty_terminal? && ImageUtil.png?(path)
|
|
445
503
|
image_id = ensure_kitty_uploaded(path)
|
|
446
|
-
@terminal.move_to(
|
|
504
|
+
@terminal.move_to(y_cell, x_cell)
|
|
447
505
|
@terminal.write ImageUtil.kitty_place(image_id: image_id, cols: target_cols, rows: target_rows)
|
|
448
506
|
elsif ImageUtil.kitty_terminal?
|
|
449
|
-
data = cached_kitty_icat(path, cols: target_cols, rows: target_rows, x:
|
|
507
|
+
data = cached_kitty_icat(path, cols: target_cols, rows: target_rows, x: x_cell - 1, y: y_cell - 1)
|
|
450
508
|
@terminal.write data if data && !data.empty?
|
|
451
509
|
elsif ImageUtil.sixel_available?
|
|
452
|
-
@terminal.move_to(
|
|
510
|
+
@terminal.move_to(y_cell, x_cell)
|
|
453
511
|
target_pixel_w = target_cols * cell_w
|
|
454
512
|
target_pixel_h = target_rows * cell_h
|
|
455
513
|
sixel = cached_sixel_encode(path, width: target_pixel_w, height: target_pixel_h)
|
|
456
514
|
@terminal.write sixel if sixel && !sixel.empty?
|
|
457
515
|
end
|
|
458
516
|
|
|
459
|
-
row + target_rows
|
|
517
|
+
absolute ? row : row + target_rows
|
|
460
518
|
end
|
|
461
519
|
|
|
462
520
|
def resolve_image_path(path)
|
|
@@ -830,11 +888,20 @@ module Przn
|
|
|
830
888
|
when :table
|
|
831
889
|
((block[:header] ? 2 : 0) + block[:rows].size) * s
|
|
832
890
|
when :image
|
|
833
|
-
|
|
891
|
+
# Absolute-positioned images (`<img x y src/>`) layer on top of
|
|
892
|
+
# the slide and contribute 0 to the layout — same treatment as :at.
|
|
893
|
+
attrs = block[:attrs] || {}
|
|
894
|
+
if resolve_at_coord(attrs['x'], @terminal.width) && resolve_at_coord(attrs['y'], @terminal.height)
|
|
895
|
+
0
|
|
896
|
+
else
|
|
897
|
+
image_block_height(block, width)
|
|
898
|
+
end
|
|
834
899
|
when :align
|
|
835
900
|
0
|
|
836
901
|
when :bg
|
|
837
902
|
0
|
|
903
|
+
when :at
|
|
904
|
+
0
|
|
838
905
|
when :blank
|
|
839
906
|
s
|
|
840
907
|
else
|
data/lib/przn/version.rb
CHANGED
data/lib/przn.rb
CHANGED
data/sample/sample.md
CHANGED
|
@@ -48,6 +48,22 @@ normal and {::tag name="red"}red text{:/tag} mixed
|
|
|
48
48
|
|
|
49
49
|
{:relative_height="70"}
|
|
50
50
|
|
|
51
|
+
# Image (XML form)
|
|
52
|
+
|
|
53
|
+
<img src="doge.png" relative_height="70"/>
|
|
54
|
+
|
|
55
|
+
# Image (absolute position)
|
|
56
|
+
|
|
57
|
+
<img src="doge.png" x="5" y="3" relative_height="40"/>
|
|
58
|
+
<img src="doge.png" x="50%" y="50%" relative_height="40"/>
|
|
59
|
+
|
|
60
|
+
# Absolute-position text
|
|
61
|
+
|
|
62
|
+
<at x="10" y="10">top-left ish</at>
|
|
63
|
+
<at x="40" y="15"><size=3>BIG</size></at>
|
|
64
|
+
<at x="80" y="25"><color=red>warn</color></at>
|
|
65
|
+
<at x="50%" y="50%">dead center</at>
|
|
66
|
+
|
|
51
67
|
# Thank You!
|
|
52
68
|
|
|
53
69
|
That's all! Enjoy!
|