przn 0.4.0 → 0.6.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 +496 -66
- data/default_theme.yml +98 -18
- data/exe/przn +8 -2
- data/lib/przn/audience_link.rb +3 -3
- data/lib/przn/code_highlighter.rb +165 -0
- data/lib/przn/controller.rb +506 -14
- data/lib/przn/echoes_client.rb +20 -0
- data/lib/przn/file_watcher.rb +69 -0
- data/lib/przn/image_util.rb +65 -2
- data/lib/przn/kitty_text.rb +2 -1
- data/lib/przn/mermaid_renderer.rb +88 -0
- data/lib/przn/parser.rb +417 -35
- data/lib/przn/prawn_pdf_exporter.rb +133 -15
- data/lib/przn/presentation.rb +43 -0
- data/lib/przn/presenter_renderer.rb +22 -8
- data/lib/przn/renderer.rb +2411 -305
- data/lib/przn/screenshot_pdf_exporter.rb +173 -5
- data/lib/przn/slide.rb +4 -2
- data/lib/przn/terminal.rb +10 -0
- data/lib/przn/theme.rb +85 -10
- data/lib/przn/version.rb +1 -1
- data/lib/przn.rb +11 -5
- data/sample/ruby-logo-LICENSE +15 -0
- data/sample/ruby.jpg +0 -0
- data/sample/ruby.png +0 -0
- data/sample/sample.md +285 -28
- data/sample/shapes.md +55 -0
- metadata +91 -77
- data/sample/doge.jpg +0 -0
- data/sample/doge.png +0 -0
data/lib/przn/image_util.rb
CHANGED
|
@@ -67,6 +67,14 @@ module Przn
|
|
|
67
67
|
ENV['TERM_PROGRAM'] == 'Echoes'
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# Echoes-specific gate for OSC 7772 features (cell-alpha slide fades,
|
|
71
|
+
# bg-color / bg-gradient, hide-pointer). Strictly checked by
|
|
72
|
+
# TERM_PROGRAM since the OSC is private — other terminals would
|
|
73
|
+
# silently drop it but the renderer still spends time emitting bytes.
|
|
74
|
+
def echoes_terminal?
|
|
75
|
+
ENV['TERM_PROGRAM'] == 'Echoes'
|
|
76
|
+
end
|
|
77
|
+
|
|
70
78
|
PNG_MAGIC = "\x89PNG\r\n\x1a\n".b.freeze
|
|
71
79
|
|
|
72
80
|
def png?(path)
|
|
@@ -85,10 +93,65 @@ module Przn
|
|
|
85
93
|
"\e_Ga=t,t=f,f=100,i=#{image_id},q=2;#{encoded}\e\\"
|
|
86
94
|
end
|
|
87
95
|
|
|
96
|
+
# Kitty Graphics Protocol: upload arbitrary image bytes inline via
|
|
97
|
+
# the direct-data transmission mode (`t=d`). Used by the shape-
|
|
98
|
+
# drawing tags (`<rect>`, `<circle>`, `<line>`, …) which build a
|
|
99
|
+
# tiny self-contained SVG document per shape and ship it here.
|
|
100
|
+
# Echoes content-sniffs the payload and routes SVGs through its
|
|
101
|
+
# native CoreGraphics rasterizer (sub-millisecond for path-only
|
|
102
|
+
# SVGs — the case shape primitives always hit). `f=100` is the
|
|
103
|
+
# default PNG format code; the sniffer overrides it for SVG.
|
|
104
|
+
def kitty_upload_inline(bytes, image_id:)
|
|
105
|
+
encoded = [bytes.to_s].pack('m0')
|
|
106
|
+
"\e_Ga=t,t=d,f=100,i=#{image_id},q=2;#{encoded}\e\\"
|
|
107
|
+
end
|
|
108
|
+
|
|
88
109
|
# Kitty Graphics Protocol: place a previously-uploaded image at the
|
|
89
110
|
# current cursor position, scaled to fit `cols` x `rows` cells.
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
# `z` sets the z-index — negative values draw the image behind
|
|
112
|
+
# text cells (used by slide background images at z: -1).
|
|
113
|
+
# `x_off` / `y_off` add a sub-cell pixel offset from the top-left
|
|
114
|
+
# of the anchor cell — this is how `<img>` gets 1-px positioning
|
|
115
|
+
# without snapping to the cell grid. Omitted (or 0) means anchor
|
|
116
|
+
# to the cell's own top-left.
|
|
117
|
+
# `C=1` (suppress cursor movement / wrap / scroll) is always set
|
|
118
|
+
# — przn is a presentation tool, never the kind of context that
|
|
119
|
+
# wants an image landing near the bottom to scroll prior rows
|
|
120
|
+
# off-screen; oversize placements clip at the pane edge instead.
|
|
121
|
+
def kitty_place(image_id:, cols:, rows:, z: nil, x_off: 0, y_off: 0)
|
|
122
|
+
params = +"a=p,i=#{image_id},c=#{cols},r=#{rows},C=1,q=2"
|
|
123
|
+
params << ",X=#{x_off}" if x_off && x_off > 0
|
|
124
|
+
params << ",Y=#{y_off}" if y_off && y_off > 0
|
|
125
|
+
params << ",z=#{z}" if z
|
|
126
|
+
"\e_G#{params}\e\\"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Kitty Graphics Protocol: delete every placement for a specific
|
|
130
|
+
# image id while keeping the stored image data alive (lowercase
|
|
131
|
+
# `d=i`). Used to wipe the previous slide's background image
|
|
132
|
+
# before placing a new one, without forcing a re-upload.
|
|
133
|
+
def kitty_delete_placements(image_id:)
|
|
134
|
+
"\e_Ga=d,d=i,i=#{image_id},q=2\e\\"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Kitty Graphics Protocol: delete every placement on the screen
|
|
138
|
+
# while keeping the stored image data alive (lowercase `d=a`).
|
|
139
|
+
# Called at the start of each slide render to wipe the previous
|
|
140
|
+
# slide's `<img>` / shape placements; the new slide re-emits its
|
|
141
|
+
# own placements so cached images need only the small placement
|
|
142
|
+
# command, not a re-transmission.
|
|
143
|
+
def kitty_delete_all_placements
|
|
144
|
+
"\e_Ga=d,d=a,q=2\e\\"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Kitty Graphics Protocol: delete every placement and free the
|
|
148
|
+
# stored image data. Used on quit so previously-rendered images
|
|
149
|
+
# don't leak through onto the user's restored shell screen
|
|
150
|
+
# (placements aren't tied to the alt-screen buffer in most
|
|
151
|
+
# kitty-protocol implementations, so leaving the alt screen
|
|
152
|
+
# alone isn't enough to hide them). `q=2` suppresses the OK reply.
|
|
153
|
+
def kitty_clear_all
|
|
154
|
+
"\e_Ga=d,d=A,q=2\e\\"
|
|
92
155
|
end
|
|
93
156
|
|
|
94
157
|
# Sixel via img2sixel
|
data/lib/przn/kitty_text.rb
CHANGED
|
@@ -19,8 +19,9 @@ module Przn
|
|
|
19
19
|
# 66, which renders without the flip / custom font on any kitty-
|
|
20
20
|
# compatible terminal (better than emitting an OSC 7772 frame the
|
|
21
21
|
# terminal would ignore entirely).
|
|
22
|
-
def sized(text, s:, h: nil, v: nil, n: nil, d: nil, f: nil, flip: nil)
|
|
22
|
+
def sized(text, s:, h: nil, v: nil, n: nil, d: nil, f: nil, flip: nil, w: nil)
|
|
23
23
|
params = +"s=#{s}"
|
|
24
|
+
params << ":w=#{w}" if w
|
|
24
25
|
params << ":n=#{n}" if n
|
|
25
26
|
params << ":d=#{d}" if d
|
|
26
27
|
params << ":h=#{h}" if h
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'shellwords'
|
|
7
|
+
|
|
8
|
+
module Przn
|
|
9
|
+
# Shell-out wrapper around the Mermaid CLI (`mmdc`). Given a Mermaid
|
|
10
|
+
# source string, returns the path to a rendered PNG — cached for the
|
|
11
|
+
# life of the session in a tmpdir keyed by SHA256 of the source so
|
|
12
|
+
# the same diagram on multiple slides (or after `r` reload) shells
|
|
13
|
+
# out exactly once.
|
|
14
|
+
#
|
|
15
|
+
# The PNG is rendered with `-b transparent` so the slide background
|
|
16
|
+
# (theme `bg`, gradient, image) shows through. Width is fixed at
|
|
17
|
+
# 1600 px so the rendered image is high-res enough for any cell
|
|
18
|
+
# placement; the Kitty Graphics protocol scales to whatever cell
|
|
19
|
+
# rect the placement carves out.
|
|
20
|
+
#
|
|
21
|
+
# When `mmdc` is missing or the render fails for any reason, returns
|
|
22
|
+
# nil and the caller falls back to rendering the source as a plain
|
|
23
|
+
# fenced block.
|
|
24
|
+
module MermaidRenderer
|
|
25
|
+
CANVAS_WIDTH_PX = 1600
|
|
26
|
+
|
|
27
|
+
@cache = {}
|
|
28
|
+
@cache_mutex = Mutex.new
|
|
29
|
+
@tmpdir = nil
|
|
30
|
+
@tmpdir_mutex = Mutex.new
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# Returns the path to a rendered PNG, or nil if rendering failed.
|
|
35
|
+
# `theme` defaults to mermaid's `default`; future hook for a theme
|
|
36
|
+
# knob (theme.yml `mermaid.theme`).
|
|
37
|
+
def render(source, theme: 'default')
|
|
38
|
+
return nil if source.nil? || source.strip.empty?
|
|
39
|
+
key = Digest::SHA256.hexdigest("#{theme}:#{source}")
|
|
40
|
+
cached = @cache_mutex.synchronize { @cache[key] }
|
|
41
|
+
return cached if cached
|
|
42
|
+
|
|
43
|
+
dir = ensure_tmpdir
|
|
44
|
+
in_path = File.join(dir, "#{key}.mmd")
|
|
45
|
+
out_path = File.join(dir, "#{key}.png")
|
|
46
|
+
File.write(in_path, source)
|
|
47
|
+
|
|
48
|
+
cmd = ['mmdc',
|
|
49
|
+
'--quiet',
|
|
50
|
+
'-i', in_path,
|
|
51
|
+
'-o', out_path,
|
|
52
|
+
'-t', theme,
|
|
53
|
+
'-b', 'transparent',
|
|
54
|
+
'-w', CANVAS_WIDTH_PX.to_s]
|
|
55
|
+
ok = system(*cmd, out: File::NULL, err: File::NULL)
|
|
56
|
+
|
|
57
|
+
return nil unless ok && File.exist?(out_path) && File.size(out_path).positive?
|
|
58
|
+
|
|
59
|
+
@cache_mutex.synchronize { @cache[key] = out_path }
|
|
60
|
+
out_path
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Bare check for whether the CLI is reachable. Used by the
|
|
66
|
+
# renderer to decide whether to even try (or to short-circuit to
|
|
67
|
+
# the plain-fence fallback).
|
|
68
|
+
def available?
|
|
69
|
+
return @available unless @available.nil?
|
|
70
|
+
@available = system('mmdc', '--version', out: File::NULL, err: File::NULL)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reset!
|
|
74
|
+
@cache_mutex.synchronize { @cache.clear }
|
|
75
|
+
@tmpdir_mutex.synchronize do
|
|
76
|
+
FileUtils.remove_entry(@tmpdir) if @tmpdir && File.directory?(@tmpdir)
|
|
77
|
+
@tmpdir = nil
|
|
78
|
+
end
|
|
79
|
+
@available = nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ensure_tmpdir
|
|
83
|
+
@tmpdir_mutex.synchronize do
|
|
84
|
+
@tmpdir ||= Dir.mktmpdir('przn-mermaid-')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|