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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'tmpdir'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'zlib'
|
|
5
7
|
|
|
6
8
|
module Przn
|
|
7
9
|
# Default PDF export: drives the live renderer, asks the terminal to save
|
|
@@ -10,11 +12,11 @@ module Przn
|
|
|
10
12
|
# Requires Echoes (or any terminal that implements the same capture
|
|
11
13
|
# command); use `export_pdf_prawn` instead for environments where that's
|
|
12
14
|
# not possible (CI, headless).
|
|
13
|
-
def self.export_pdf(file, output, theme: nil)
|
|
15
|
+
def self.export_pdf(file, output, theme: nil, quality: nil)
|
|
14
16
|
markdown = File.read(file)
|
|
15
17
|
presentation = Parser.parse(markdown)
|
|
16
18
|
base_dir = File.dirname(File.expand_path(file))
|
|
17
|
-
ScreenshotPdfExporter.new(presentation, base_dir: base_dir, theme: theme).export(output)
|
|
19
|
+
ScreenshotPdfExporter.new(presentation, base_dir: base_dir, theme: theme, quality: quality).export(output)
|
|
18
20
|
puts "Generated: #{output}"
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -43,12 +45,27 @@ module Przn
|
|
|
43
45
|
POLL_INTERVAL = 0.05 # seconds between file-existence checks
|
|
44
46
|
CAPTURE_TIMEOUT = 10 # seconds per slide before giving up
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
# Maps the user-facing `--pdf-quality` flag onto Ghostscript's
|
|
49
|
+
# `-dPDFSETTINGS` preset names. `:lossless` skips the gs pass
|
|
50
|
+
# entirely (lossless HexaPDF layers only — useful when image
|
|
51
|
+
# fidelity matters more than file size). `nil` means "use the
|
|
52
|
+
# default", which is `:medium`.
|
|
53
|
+
QUALITY_PRESETS = {
|
|
54
|
+
'lossless' => :lossless,
|
|
55
|
+
'low' => '/screen',
|
|
56
|
+
'medium' => '/ebook',
|
|
57
|
+
'high' => '/printer',
|
|
58
|
+
'max' => '/prepress',
|
|
59
|
+
}.freeze
|
|
60
|
+
DEFAULT_QUALITY = 'medium'
|
|
61
|
+
|
|
62
|
+
def initialize(presentation, base_dir: '.', theme: nil, terminal: nil, quality: nil)
|
|
47
63
|
@presentation = presentation
|
|
48
64
|
@base_dir = base_dir
|
|
49
65
|
@theme = theme || Theme.default
|
|
50
66
|
@terminal = terminal || Terminal.new
|
|
51
|
-
@renderer = Renderer.new(@terminal, base_dir: base_dir, theme: theme)
|
|
67
|
+
@renderer = Renderer.new(@terminal, base_dir: base_dir, theme: theme, export_mode: true, presentation: presentation)
|
|
68
|
+
@quality = resolve_quality(quality)
|
|
52
69
|
end
|
|
53
70
|
|
|
54
71
|
def export(output_path)
|
|
@@ -68,7 +85,10 @@ module Przn
|
|
|
68
85
|
@terminal.hide_cursor
|
|
69
86
|
@presentation.slides.each_with_index do |slide, i|
|
|
70
87
|
pdf_path = File.join(dir, format('slide-%04d.pdf', i))
|
|
71
|
-
|
|
88
|
+
# Render at the final reveal — a PDF is a static artifact, so
|
|
89
|
+
# every `<wait/>` step should already be visible on the page.
|
|
90
|
+
final_step = @renderer.step_count(slide) - 1
|
|
91
|
+
@renderer.render(slide, current: i, total: @presentation.total, step: final_step)
|
|
72
92
|
request_capture(pdf_path)
|
|
73
93
|
wait_for_capture(pdf_path)
|
|
74
94
|
paths << pdf_path
|
|
@@ -76,6 +96,7 @@ module Przn
|
|
|
76
96
|
paths
|
|
77
97
|
ensure
|
|
78
98
|
@terminal.write "#{OSC};bg-clear#{BEL}"
|
|
99
|
+
@terminal.write ImageUtil.kitty_clear_all if ImageUtil.kitty_terminal?
|
|
79
100
|
@terminal.show_cursor
|
|
80
101
|
@terminal.leave_alt_screen
|
|
81
102
|
@terminal.flush
|
|
@@ -109,7 +130,154 @@ module Przn
|
|
|
109
130
|
output.pages << output.import(page)
|
|
110
131
|
end
|
|
111
132
|
end
|
|
133
|
+
|
|
134
|
+
# Three lossless passes before write. Page surfaces stay
|
|
135
|
+
# byte-identical; only the encoding of the underlying objects
|
|
136
|
+
# shrinks.
|
|
137
|
+
# 1. Dedup image XObjects — repeated logos / shared backgrounds
|
|
138
|
+
# become a single shared object.
|
|
139
|
+
# 2. Re-Flate uncompressed image streams.
|
|
140
|
+
# 3. HexaPDF's task(:optimize) collapses metadata into object
|
|
141
|
+
# streams and rewrites xref as binary streams.
|
|
142
|
+
dedup_image_xobjects(output)
|
|
143
|
+
reflate_uncompressed_images(output)
|
|
144
|
+
output.task(:optimize,
|
|
145
|
+
compact: true,
|
|
146
|
+
object_streams: :generate,
|
|
147
|
+
xref_streams: :generate)
|
|
148
|
+
|
|
112
149
|
output.write(output_path)
|
|
150
|
+
|
|
151
|
+
# Optional lossy post-process. Echoes' OSC 7772 capture embeds
|
|
152
|
+
# `<img>` and shape rasters at their NATIVE source resolution
|
|
153
|
+
# (a 24-megapixel phone photo embedded as a slide thumbnail
|
|
154
|
+
# stays at 24MP in the captured PDF). Re-Flating that pixel
|
|
155
|
+
# data, as the three lossless passes above do, doesn't help —
|
|
156
|
+
# photographic content compresses 10-50× better as JPEG.
|
|
157
|
+
#
|
|
158
|
+
# Ghostscript's `-dPDFSETTINGS=/ebook` downsamples raster
|
|
159
|
+
# images to 150 DPI and re-encodes them as JPEG. We shell out
|
|
160
|
+
# if `gs` is on PATH; if not, the file is left as-is with a
|
|
161
|
+
# friendly hint on stderr. Lossy in the strict sense, but the
|
|
162
|
+
# `/ebook` preset is widely considered visually indistinguishable
|
|
163
|
+
# at presentation viewing distances.
|
|
164
|
+
shrink_with_ghostscript(output_path)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Resolve the Ghostscript binary path the recompression pass will
|
|
168
|
+
# use. Three layers, in order:
|
|
169
|
+
# 1. `PRZN_GS=` (explicitly empty) — opt out, no warning. Used
|
|
170
|
+
# by tests and by anyone who wants the lossless-only pipeline.
|
|
171
|
+
# 2. `PRZN_GS=<path>` — explicit override (custom build, brew
|
|
172
|
+
# keg-only path, etc).
|
|
173
|
+
# 3. Default: probe PATH for `gs`. Missing binary → returns
|
|
174
|
+
# :missing so the caller can print the install hint once.
|
|
175
|
+
def gs_path
|
|
176
|
+
env = ENV['PRZN_GS']
|
|
177
|
+
return :disabled if env == ''
|
|
178
|
+
bin = env && !env.empty? ? env : 'gs'
|
|
179
|
+
found = `which #{bin} 2>/dev/null`.chomp
|
|
180
|
+
found.empty? ? :missing : found
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def shrink_with_ghostscript(path)
|
|
184
|
+
return if @quality == :lossless
|
|
185
|
+
gs = gs_path
|
|
186
|
+
case gs
|
|
187
|
+
when :disabled then return
|
|
188
|
+
when :missing
|
|
189
|
+
warn 'Hint: install Ghostscript (`brew install ghostscript`) for a 5-20× ' \
|
|
190
|
+
'size reduction on image-heavy decks. Skipping recompression. ' \
|
|
191
|
+
'Pass --pdf-quality lossless to silence this hint.'
|
|
192
|
+
return
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
tmp = "#{path}.gs.tmp"
|
|
196
|
+
ok = system(gs,
|
|
197
|
+
'-q',
|
|
198
|
+
'-dQUIET', '-dNOPAUSE', '-dBATCH', '-dSAFER',
|
|
199
|
+
'-sDEVICE=pdfwrite',
|
|
200
|
+
"-dPDFSETTINGS=#{@quality}",
|
|
201
|
+
'-dCompatibilityLevel=1.5',
|
|
202
|
+
"-sOutputFile=#{tmp}",
|
|
203
|
+
path,
|
|
204
|
+
out: File::NULL, err: File::NULL)
|
|
205
|
+
if ok && File.exist?(tmp) && File.size(tmp).positive? && File.size(tmp) < File.size(path)
|
|
206
|
+
FileUtils.mv(tmp, path)
|
|
207
|
+
else
|
|
208
|
+
FileUtils.rm_f(tmp)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def resolve_quality(raw)
|
|
213
|
+
key = (raw || DEFAULT_QUALITY).to_s.downcase
|
|
214
|
+
preset = QUALITY_PRESETS[key]
|
|
215
|
+
unless preset
|
|
216
|
+
raise ArgumentError,
|
|
217
|
+
"unknown --pdf-quality #{raw.inspect}; expected one of " \
|
|
218
|
+
"#{QUALITY_PRESETS.keys.join(', ')}"
|
|
219
|
+
end
|
|
220
|
+
preset
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Find image XObjects whose stream bytes are byte-for-byte identical
|
|
224
|
+
# and collapse them into a single shared object — the case being:
|
|
225
|
+
# a slide background or repeated logo gets captured into N
|
|
226
|
+
# per-slide PDFs independently, then HexaPDF imports each one as
|
|
227
|
+
# its own object. Hashing the stream bytes is enough to recognise
|
|
228
|
+
# them; the later pages' XObject resource entries are pointed at
|
|
229
|
+
# the first occurrence's indirect reference and the duplicate
|
|
230
|
+
# objects are swept by the subsequent compact-optimize pass.
|
|
231
|
+
def dedup_image_xobjects(doc)
|
|
232
|
+
by_hash = {}
|
|
233
|
+
doc.pages.each do |page|
|
|
234
|
+
resources = page[:Resources]
|
|
235
|
+
next unless resources && resources[:XObject]
|
|
236
|
+
xobjects = resources[:XObject]
|
|
237
|
+
xobjects.each do |name, xobj|
|
|
238
|
+
next unless image_xobject?(xobj)
|
|
239
|
+
h = Digest::SHA256.hexdigest(xobj.stream.to_s)
|
|
240
|
+
if (canonical = by_hash[h])
|
|
241
|
+
xobjects[name] = canonical unless canonical.equal?(xobj)
|
|
242
|
+
else
|
|
243
|
+
by_hash[h] = xobj
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
# Sweep the now-orphaned duplicates.
|
|
248
|
+
doc.task(:optimize, compact: true)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Apply best-strength Flate to image streams that arrived
|
|
252
|
+
# uncompressed (or compressed by a filter Flate can outperform).
|
|
253
|
+
# JPEG (`DCTDecode`) and JPEG-2000 (`JPXDecode`) streams are
|
|
254
|
+
# already entropy-coded — re-encoding them losslessly cannot
|
|
255
|
+
# help and lossy re-encoding would need an external image
|
|
256
|
+
# library. Streams that grow under Flate (rare; happens when
|
|
257
|
+
# the input is already near maximum entropy) are left alone.
|
|
258
|
+
def reflate_uncompressed_images(doc)
|
|
259
|
+
doc.each do |obj|
|
|
260
|
+
next unless image_xobject?(obj)
|
|
261
|
+
filter = Array(obj[:Filter])
|
|
262
|
+
next if filter.include?(:FlateDecode) ||
|
|
263
|
+
filter.include?(:DCTDecode) ||
|
|
264
|
+
filter.include?(:JPXDecode)
|
|
265
|
+
raw = obj.stream.to_s
|
|
266
|
+
next if raw.empty?
|
|
267
|
+
deflated = Zlib::Deflate.deflate(raw, Zlib::BEST_COMPRESSION)
|
|
268
|
+
next if deflated.bytesize >= raw.bytesize
|
|
269
|
+
obj.stream = deflated
|
|
270
|
+
obj[:Filter] = :FlateDecode
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def image_xobject?(obj)
|
|
275
|
+
# `doc.each` walks every indirect object, including HexaPDF::PDFArray
|
|
276
|
+
# whose `[]` raises on a Symbol key. Gate on the underlying value
|
|
277
|
+
# being a dict (Hash) before any keyed lookup so arrays and plain
|
|
278
|
+
# scalars are silently skipped.
|
|
279
|
+
return false unless obj.respond_to?(:value) && obj.value.is_a?(Hash)
|
|
280
|
+
obj[:Type] == :XObject && obj[:Subtype] == :Image
|
|
113
281
|
end
|
|
114
282
|
end
|
|
115
283
|
end
|
data/lib/przn/slide.rb
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Przn
|
|
4
4
|
class Slide
|
|
5
|
-
attr_reader :blocks
|
|
5
|
+
attr_reader :blocks, :layout, :attrs
|
|
6
6
|
|
|
7
|
-
def initialize(blocks)
|
|
7
|
+
def initialize(blocks, layout: nil, attrs: {})
|
|
8
8
|
@blocks = blocks.freeze
|
|
9
|
+
@layout = layout
|
|
10
|
+
@attrs = attrs.freeze
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
# Aggregate every `{::note}` / `<note>` segment in the slide's text-bearing
|
data/lib/przn/terminal.rb
CHANGED
|
@@ -5,6 +5,10 @@ require 'io/console'
|
|
|
5
5
|
module Przn
|
|
6
6
|
class Terminal
|
|
7
7
|
MOUSE_OFF = "\e[?1006l\e[?1003l\e[?1002l\e[?1000l"
|
|
8
|
+
# Button-event tracking (1002: presses, releases, and motion only
|
|
9
|
+
# while a button is held — for dragging the 🐇) in SGR encoding
|
|
10
|
+
# (1006). Bare hovering is never reported.
|
|
11
|
+
MOUSE_ON = "\e[?1002h\e[?1006h"
|
|
8
12
|
|
|
9
13
|
def initialize(input: $stdin, output: $stdout)
|
|
10
14
|
@in = input
|
|
@@ -50,6 +54,12 @@ module Przn
|
|
|
50
54
|
write MOUSE_OFF
|
|
51
55
|
end
|
|
52
56
|
|
|
57
|
+
# Report clicks so the controller can page with the mouse. Cleared
|
|
58
|
+
# by `leave_alt_screen`'s MOUSE_OFF.
|
|
59
|
+
def enable_mouse
|
|
60
|
+
write MOUSE_ON
|
|
61
|
+
end
|
|
62
|
+
|
|
53
63
|
def leave_alt_screen
|
|
54
64
|
write MOUSE_OFF
|
|
55
65
|
write "\e[?1049l"
|
data/lib/przn/theme.rb
CHANGED
|
@@ -6,7 +6,26 @@ module Przn
|
|
|
6
6
|
class Theme
|
|
7
7
|
DEFAULT_PATH = File.expand_path('../../../default_theme.yml', __FILE__)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# A rectangular region inside a slide. `x` / `y` are stored as the
|
|
10
|
+
# raw strings from the YAML and resolved at render time against
|
|
11
|
+
# the current terminal width / height by the same helper that
|
|
12
|
+
# handles `<at x y>` and `<img x y>` coords — so `x` accepts the
|
|
13
|
+
# full vocabulary (cell, `%`, `c`, `px`, and the keyword forms
|
|
14
|
+
# `left` / `center` / `right`). When `x` is a keyword, the slot's
|
|
15
|
+
# effective starting column is computed against `width` AND the
|
|
16
|
+
# alignment used for blocks routed into the slot is derived from
|
|
17
|
+
# the same keyword (`x: center` → block align: center, slot
|
|
18
|
+
# positioned so its centre lands on the slide's centre).
|
|
19
|
+
#
|
|
20
|
+
# `size` / `family` / `color` are per-slot text-style overrides:
|
|
21
|
+
# `size` sets the base scale for h1 and paragraphs (accepts the
|
|
22
|
+
# same names as inline `<size>`); `family` and `color` cascade
|
|
23
|
+
# through render_segments_scaled to every text run in the slot.
|
|
24
|
+
# Inline tags (`<size>`, `<font>`, `<color>`) still win
|
|
25
|
+
# per-segment.
|
|
26
|
+
Slot = Struct.new(:name, :x, :y, :width, :height, :size, :family, :color)
|
|
27
|
+
|
|
28
|
+
attr_reader :colors, :font, :bullet, :background, :title, :counter, :code, :layouts, :transition, :fragment
|
|
10
29
|
|
|
11
30
|
def self.load(path)
|
|
12
31
|
raise ArgumentError, "Theme file not found: #{path}" unless File.exist?(path)
|
|
@@ -19,11 +38,30 @@ module Przn
|
|
|
19
38
|
bullet: defaults[:bullet].merge(overrides[:bullet] || {}),
|
|
20
39
|
background: defaults[:background].merge(overrides[:background] || {}),
|
|
21
40
|
title: defaults[:title].merge(overrides[:title] || {}),
|
|
22
|
-
# `
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
# `code` styles fenced code blocks (family / size / color / bg).
|
|
42
|
+
# Unset keys fall through to: family → font.family, size →
|
|
43
|
+
# body_scale, color → terminal default fg, bg → the legacy dim
|
|
44
|
+
# gray ANSI 236.
|
|
45
|
+
code: defaults[:code].merge(overrides[:code] || {}),
|
|
46
|
+
# `counter` is always a hash; empty by default. `counter.duration`
|
|
47
|
+
# opts into the 🐇 runner bar; without it the renderer shows the
|
|
48
|
+
# plain " N / M " footer. `counter.color` styles both modes.
|
|
49
|
+
counter: defaults[:counter].merge(overrides[:counter] || {}),
|
|
50
|
+
# `transition` carries the slide-fade `duration:` (ms) used when
|
|
51
|
+
# a slide opts in via `# Title {transition=fade}`. Empty by
|
|
52
|
+
# default — the renderer's fallback duration kicks in.
|
|
53
|
+
transition: defaults[:transition].merge(overrides[:transition] || {}),
|
|
54
|
+
# `fragment` carries the per-fragment animation `duration:` (ms)
|
|
55
|
+
# used by `<at fragment="fade-up">` and friends. Empty hash if the
|
|
56
|
+
# user opts out entirely; the renderer falls back to its hardcoded
|
|
57
|
+
# 300ms when the key is missing.
|
|
58
|
+
fragment: defaults[:fragment].merge(overrides[:fragment] || {}),
|
|
59
|
+
# Layouts: per-name replacement (no deep merge across slot lists —
|
|
60
|
+
# an override redefines that layout's slot list end-to-end). The
|
|
61
|
+
# name `default` is special: when a slide has no `{layout=...}`
|
|
62
|
+
# IAL the renderer looks it up here. Themes that don't define
|
|
63
|
+
# `layouts.default` keep the legacy flow behavior.
|
|
64
|
+
layouts: defaults[:layouts].merge(overrides[:layouts] || {})
|
|
27
65
|
}
|
|
28
66
|
new(merged)
|
|
29
67
|
end
|
|
@@ -71,20 +109,57 @@ module Przn
|
|
|
71
109
|
bullet: (data[:bullet] || {}).compact,
|
|
72
110
|
background: (data[:background] || {}).compact,
|
|
73
111
|
title: (data[:title] || {}).compact,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
112
|
+
code: (data[:code] || {}).compact,
|
|
113
|
+
counter: (data[:counter] || {}).compact,
|
|
114
|
+
transition: (data[:transition] || {}).compact,
|
|
115
|
+
fragment: (data[:fragment] || {}).compact,
|
|
116
|
+
layouts: parse_layouts(data[:layouts])
|
|
77
117
|
}
|
|
78
118
|
end
|
|
79
119
|
private_class_method :load_yaml
|
|
80
120
|
|
|
121
|
+
# Turn the raw `layouts:` YAML into a `{name => [Slot, ...]}` map.
|
|
122
|
+
# Each layout's value is just the ordered slot list (no enclosing
|
|
123
|
+
# `slots:` key in v1 since there's no other per-layout metadata
|
|
124
|
+
# — promoting to a hash later is a one-line shape change).
|
|
125
|
+
def self.parse_layouts(raw)
|
|
126
|
+
return {} unless raw.is_a?(Hash)
|
|
127
|
+
|
|
128
|
+
raw.each_with_object({}) do |(name, slot_list), out|
|
|
129
|
+
next unless slot_list.is_a?(Array)
|
|
130
|
+
out[name.to_s] = slot_list.map { |s|
|
|
131
|
+
h = s.transform_keys(&:to_sym)
|
|
132
|
+
# Back-compat: a legacy `align: center` (etc.) on a slot
|
|
133
|
+
# used to be a separate knob from `x:`. The two were always
|
|
134
|
+
# used together in practice — every built-in layout that
|
|
135
|
+
# set `align:` paired it with an `x:` whose only job was
|
|
136
|
+
# to leave room for the centred content. The new shape
|
|
137
|
+
# collapses them into `x: <keyword>`, so an old `align:`
|
|
138
|
+
# value overrides any numeric `x:` written alongside it.
|
|
139
|
+
x = h[:align] ? h[:align].to_s : h[:x].to_s
|
|
140
|
+
Slot.new(
|
|
141
|
+
h[:name].to_s,
|
|
142
|
+
x, h[:y].to_s, h[:width].to_s, h[:height].to_s,
|
|
143
|
+
h[:size]&.to_s,
|
|
144
|
+
h[:family]&.to_s,
|
|
145
|
+
h[:color]&.to_s
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
private_class_method :parse_layouts
|
|
151
|
+
|
|
81
152
|
def initialize(config)
|
|
82
153
|
@colors = config[:colors]
|
|
83
154
|
@font = config[:font]
|
|
84
155
|
@bullet = config[:bullet]
|
|
85
156
|
@background = config[:background]
|
|
86
157
|
@title = config[:title]
|
|
87
|
-
@
|
|
158
|
+
@code = config[:code] || {}
|
|
159
|
+
@counter = config[:counter] || {}
|
|
160
|
+
@transition = config[:transition] || {}
|
|
161
|
+
@fragment = config[:fragment] || {}
|
|
162
|
+
@layouts = config[:layouts] || {}
|
|
88
163
|
end
|
|
89
164
|
end
|
|
90
165
|
end
|
data/lib/przn/version.rb
CHANGED
data/lib/przn.rb
CHANGED
|
@@ -7,6 +7,8 @@ require_relative 'przn/version'
|
|
|
7
7
|
require_relative 'przn/kitty_text'
|
|
8
8
|
require_relative 'przn/image_util'
|
|
9
9
|
require_relative 'przn/parser'
|
|
10
|
+
require_relative 'przn/code_highlighter'
|
|
11
|
+
require_relative 'przn/mermaid_renderer'
|
|
10
12
|
require_relative 'przn/slide'
|
|
11
13
|
require_relative 'przn/presentation'
|
|
12
14
|
require_relative 'przn/terminal'
|
|
@@ -14,6 +16,7 @@ require_relative 'przn/renderer'
|
|
|
14
16
|
require_relative 'przn/presenter_renderer'
|
|
15
17
|
require_relative 'przn/audience_link'
|
|
16
18
|
require_relative 'przn/echoes_client'
|
|
19
|
+
require_relative 'przn/file_watcher'
|
|
17
20
|
require_relative 'przn/controller'
|
|
18
21
|
require_relative 'przn/screenshot_pdf_exporter'
|
|
19
22
|
require_relative 'przn/theme'
|
|
@@ -21,14 +24,15 @@ require_relative 'przn/theme'
|
|
|
21
24
|
module Przn
|
|
22
25
|
class Error < StandardError; end
|
|
23
26
|
|
|
24
|
-
def self.start(file, theme: nil, start_at: nil)
|
|
27
|
+
def self.start(file, theme: nil, theme_path: nil, start_at: nil, watch: true)
|
|
25
28
|
markdown = File.read(file)
|
|
26
29
|
presentation = Parser.parse(markdown)
|
|
27
30
|
presentation.go_to(start_at - 1) if start_at
|
|
28
31
|
terminal = Terminal.new
|
|
29
32
|
base_dir = File.dirname(File.expand_path(file))
|
|
30
|
-
renderer = Renderer.new(terminal, base_dir: base_dir, theme: theme)
|
|
31
|
-
Controller.new(presentation, terminal, renderer
|
|
33
|
+
renderer = Renderer.new(terminal, base_dir: base_dir, theme: theme, presentation: presentation)
|
|
34
|
+
Controller.new(presentation, terminal, renderer,
|
|
35
|
+
source_file: file, theme_path: theme_path, watch: watch)
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
# Audience-side entry: opens the file, listens on `socket`, and renders
|
|
@@ -39,7 +43,7 @@ module Przn
|
|
|
39
43
|
presentation = Parser.parse(markdown)
|
|
40
44
|
terminal = Terminal.new
|
|
41
45
|
base_dir = File.dirname(File.expand_path(file))
|
|
42
|
-
renderer = Renderer.new(terminal, base_dir: base_dir, theme: theme, mode: :audience)
|
|
46
|
+
renderer = Renderer.new(terminal, base_dir: base_dir, theme: theme, mode: :audience, presentation: presentation)
|
|
43
47
|
|
|
44
48
|
terminal.enter_alt_screen
|
|
45
49
|
terminal.hide_cursor
|
|
@@ -59,6 +63,7 @@ module Przn
|
|
|
59
63
|
end
|
|
60
64
|
ensure
|
|
61
65
|
terminal.write "\e]7772;bg-clear\a"
|
|
66
|
+
terminal.write ImageUtil.kitty_clear_all if ImageUtil.kitty_terminal?
|
|
62
67
|
terminal.show_cursor
|
|
63
68
|
terminal.leave_alt_screen
|
|
64
69
|
end
|
|
@@ -97,6 +102,7 @@ module Przn
|
|
|
97
102
|
terminal = Terminal.new
|
|
98
103
|
base_dir = File.dirname(File.expand_path(file))
|
|
99
104
|
renderer = PresenterRenderer.new(terminal, presentation: presentation, base_dir: base_dir, theme: theme)
|
|
100
|
-
Controller.new(presentation, terminal, renderer, audience_link: link
|
|
105
|
+
Controller.new(presentation, terminal, renderer, audience_link: link,
|
|
106
|
+
source_file: file, theme_path: theme_path)
|
|
101
107
|
end
|
|
102
108
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
The Ruby Logo is Copyright (c) 2006, Yukihiro Matsumoto. It is licensed
|
|
2
|
+
under the terms of the Creative Commons Attribution-ShareAlike 2.5
|
|
3
|
+
agreement:
|
|
4
|
+
|
|
5
|
+
http://creativecommons.org/licenses/by-sa/2.5/
|
|
6
|
+
|
|
7
|
+
We ask that you do not use the logo to represent something other than
|
|
8
|
+
the Ruby Programming language. If you have questions about the logo,
|
|
9
|
+
please join the VIT-Discuss mailing list and ask your questions there:
|
|
10
|
+
|
|
11
|
+
http://rubyforge.org/mailman/listinfo/vit-discuss/
|
|
12
|
+
|
|
13
|
+
Thank you,
|
|
14
|
+
|
|
15
|
+
The Ruby Visual Identity Team
|
data/sample/ruby.jpg
ADDED
|
Binary file
|
data/sample/ruby.png
ADDED
|
Binary file
|