presently 0.1.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/bin/presently +13 -0
- data/lib/presently/application.rb +104 -0
- data/lib/presently/clock.rb +77 -0
- data/lib/presently/display_view.rb +73 -0
- data/lib/presently/environment/application.rb +62 -0
- data/lib/presently/page.rb +38 -0
- data/lib/presently/page.xrb +31 -0
- data/lib/presently/presentation.rb +72 -0
- data/lib/presently/presentation_controller.rb +181 -0
- data/lib/presently/presenter_view.rb +264 -0
- data/lib/presently/slide.rb +148 -0
- data/lib/presently/slide_view.rb +92 -0
- data/lib/presently/state.rb +66 -0
- data/lib/presently/version.rb +9 -0
- data/lib/presently.rb +9 -0
- data/license.md +21 -0
- data/public/_components/@socketry/syntax/Syntax/CodeElement.js +337 -0
- data/public/_components/@socketry/syntax/Syntax/Errors.js +52 -0
- data/public/_components/@socketry/syntax/Syntax/Language/apache.js +49 -0
- data/public/_components/@socketry/syntax/Syntax/Language/applescript.js +157 -0
- data/public/_components/@socketry/syntax/Syntax/Language/assembly.js +42 -0
- data/public/_components/@socketry/syntax/Syntax/Language/bash-script.js +108 -0
- data/public/_components/@socketry/syntax/Syntax/Language/bash.js +32 -0
- data/public/_components/@socketry/syntax/Syntax/Language/basic.js +232 -0
- data/public/_components/@socketry/syntax/Syntax/Language/c++.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/c.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/clang.js +201 -0
- data/public/_components/@socketry/syntax/Syntax/Language/cpp.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/csharp.js +166 -0
- data/public/_components/@socketry/syntax/Syntax/Language/css.js +244 -0
- data/public/_components/@socketry/syntax/Syntax/Language/diff.js +24 -0
- data/public/_components/@socketry/syntax/Syntax/Language/go.js +135 -0
- data/public/_components/@socketry/syntax/Syntax/Language/haskell.js +110 -0
- data/public/_components/@socketry/syntax/Syntax/Language/html.js +69 -0
- data/public/_components/@socketry/syntax/Syntax/Language/io.js +68 -0
- data/public/_components/@socketry/syntax/Syntax/Language/java.js +134 -0
- data/public/_components/@socketry/syntax/Syntax/Language/javascript.js +89 -0
- data/public/_components/@socketry/syntax/Syntax/Language/json.js +36 -0
- data/public/_components/@socketry/syntax/Syntax/Language/lisp.js +38 -0
- data/public/_components/@socketry/syntax/Syntax/Language/lua.js +87 -0
- data/public/_components/@socketry/syntax/Syntax/Language/markdown.js +112 -0
- data/public/_components/@socketry/syntax/Syntax/Language/nginx.js +37 -0
- data/public/_components/@socketry/syntax/Syntax/Language/objective-c.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/ocaml.js +225 -0
- data/public/_components/@socketry/syntax/Syntax/Language/pascal.js +166 -0
- data/public/_components/@socketry/syntax/Syntax/Language/patch.js +2 -0
- data/public/_components/@socketry/syntax/Syntax/Language/perl5.js +317 -0
- data/public/_components/@socketry/syntax/Syntax/Language/php-script.js +112 -0
- data/public/_components/@socketry/syntax/Syntax/Language/php.js +18 -0
- data/public/_components/@socketry/syntax/Syntax/Language/plain.js +20 -0
- data/public/_components/@socketry/syntax/Syntax/Language/protobuf.js +77 -0
- data/public/_components/@socketry/syntax/Syntax/Language/python.js +208 -0
- data/public/_components/@socketry/syntax/Syntax/Language/ruby.js +124 -0
- data/public/_components/@socketry/syntax/Syntax/Language/scala.js +81 -0
- data/public/_components/@socketry/syntax/Syntax/Language/smalltalk.js +30 -0
- data/public/_components/@socketry/syntax/Syntax/Language/sql.js +865 -0
- data/public/_components/@socketry/syntax/Syntax/Language/super-collider.js +70 -0
- data/public/_components/@socketry/syntax/Syntax/Language/swift.js +176 -0
- data/public/_components/@socketry/syntax/Syntax/Language/xml.js +76 -0
- data/public/_components/@socketry/syntax/Syntax/Language/xrb.js +33 -0
- data/public/_components/@socketry/syntax/Syntax/Language/yaml.js +29 -0
- data/public/_components/@socketry/syntax/Syntax/Language.js +276 -0
- data/public/_components/@socketry/syntax/Syntax/Loader.js +78 -0
- data/public/_components/@socketry/syntax/Syntax/Match.js +546 -0
- data/public/_components/@socketry/syntax/Syntax/Rule.js +306 -0
- data/public/_components/@socketry/syntax/Syntax.js +356 -0
- data/public/_components/@socketry/syntax/bin/syntax-ast.js +42 -0
- data/public/_components/@socketry/syntax/examples/_template.html +53 -0
- data/public/_components/@socketry/syntax/examples/apache.html +72 -0
- data/public/_components/@socketry/syntax/examples/applescript.html +72 -0
- data/public/_components/@socketry/syntax/examples/assembly.html +74 -0
- data/public/_components/@socketry/syntax/examples/bash.html +90 -0
- data/public/_components/@socketry/syntax/examples/basic.html +87 -0
- data/public/_components/@socketry/syntax/examples/c.html +141 -0
- data/public/_components/@socketry/syntax/examples/clang.html +202 -0
- data/public/_components/@socketry/syntax/examples/csharp.html +110 -0
- data/public/_components/@socketry/syntax/examples/css-colors.html +179 -0
- data/public/_components/@socketry/syntax/examples/custom-theme.html +155 -0
- data/public/_components/@socketry/syntax/examples/diff.html +142 -0
- data/public/_components/@socketry/syntax/examples/examples.css +216 -0
- data/public/_components/@socketry/syntax/examples/go.html +413 -0
- data/public/_components/@socketry/syntax/examples/haskell.html +373 -0
- data/public/_components/@socketry/syntax/examples/html.html +316 -0
- data/public/_components/@socketry/syntax/examples/index.html +97 -0
- data/public/_components/@socketry/syntax/examples/io.html +552 -0
- data/public/_components/@socketry/syntax/examples/java.html +786 -0
- data/public/_components/@socketry/syntax/examples/javascript.html +199 -0
- data/public/_components/@socketry/syntax/examples/json.html +150 -0
- data/public/_components/@socketry/syntax/examples/lisp.html +476 -0
- data/public/_components/@socketry/syntax/examples/lua.html +737 -0
- data/public/_components/@socketry/syntax/examples/markdown.html +121 -0
- data/public/_components/@socketry/syntax/examples/mixed.html +306 -0
- data/public/_components/@socketry/syntax/examples/nginx.html +554 -0
- data/public/_components/@socketry/syntax/examples/ocaml.html +596 -0
- data/public/_components/@socketry/syntax/examples/pascal.html +762 -0
- data/public/_components/@socketry/syntax/examples/perl5.html +488 -0
- data/public/_components/@socketry/syntax/examples/php-script.html +142 -0
- data/public/_components/@socketry/syntax/examples/php.html +95 -0
- data/public/_components/@socketry/syntax/examples/plain.html +222 -0
- data/public/_components/@socketry/syntax/examples/protobuf.html +405 -0
- data/public/_components/@socketry/syntax/examples/python.html +82 -0
- data/public/_components/@socketry/syntax/examples/readme.md +79 -0
- data/public/_components/@socketry/syntax/examples/ruby.html +58 -0
- data/public/_components/@socketry/syntax/examples/scala.html +41 -0
- data/public/_components/@socketry/syntax/examples/smalltalk.html +436 -0
- data/public/_components/@socketry/syntax/examples/sql.html +373 -0
- data/public/_components/@socketry/syntax/examples/super-collider.html +55 -0
- data/public/_components/@socketry/syntax/examples/swift.html +176 -0
- data/public/_components/@socketry/syntax/examples/wrap-demo.html +103 -0
- data/public/_components/@socketry/syntax/examples/xml.html +112 -0
- data/public/_components/@socketry/syntax/examples/xrb.html +37 -0
- data/public/_components/@socketry/syntax/examples/yaml.html +72 -0
- data/public/_components/@socketry/syntax/license.md +21 -0
- data/public/_components/@socketry/syntax/package-lock.json +834 -0
- data/public/_components/@socketry/syntax/package.json +43 -0
- data/public/_components/@socketry/syntax/readme.md +162 -0
- data/public/_components/@socketry/syntax/test/Syntax/CodeElement.js +306 -0
- data/public/_components/@socketry/syntax/test/Syntax/ErrorHandling.js +85 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/apache.js +153 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/applescript.js +198 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/assembly.js +209 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/bash-script.js +225 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/bash.js +162 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/basic.js +265 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/clang.js +390 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/csharp.js +436 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/css.js +431 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/diff.js +206 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/go.js +386 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/haskell.js +454 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/html.js +111 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/io.js +229 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/java.js +362 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/javascript.js +101 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/json.js +101 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/lisp.js +224 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/lua.js +307 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/markdown.js +163 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/nginx.js +267 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/ocaml.js +299 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/pascal.js +311 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/perl5.js +333 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/php-script.js +197 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/php.js +92 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/plain.js +327 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/protobuf.js +294 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/python.js +213 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/ruby.js +70 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/scala.js +75 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/smalltalk.js +223 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/sql.js +281 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/super-collider.js +66 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/swift.js +71 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/xml.js +170 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/xrb.js +57 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/yaml.js +123 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language.js +62 -0
- data/public/_components/@socketry/syntax/test/Syntax/Match.js +40 -0
- data/public/_components/@socketry/syntax/test/Syntax/Rule.js +251 -0
- data/public/_components/@socketry/syntax/test/Syntax.js +38 -0
- data/public/_components/@socketry/syntax/test/helpers/ast-matcher.js +90 -0
- data/public/_components/@socketry/syntax/themes/base/apache.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/applescript.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/assembly.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/bash.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/basic.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/c.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/clang.css +0 -0
- data/public/_components/@socketry/syntax/themes/base/csharp.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/css.css +22 -0
- data/public/_components/@socketry/syntax/themes/base/diff.css +48 -0
- data/public/_components/@socketry/syntax/themes/base/go.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/haskell.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/html.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/io.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/java.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/javascript.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/json.css +41 -0
- data/public/_components/@socketry/syntax/themes/base/lisp.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/lua.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/markdown.css +16 -0
- data/public/_components/@socketry/syntax/themes/base/nginx.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/ocaml.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/pascal.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/perl5.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/php-script.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/php.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/plain.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/protobuf.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/python.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/ruby.css +23 -0
- data/public/_components/@socketry/syntax/themes/base/scala.css +3 -0
- data/public/_components/@socketry/syntax/themes/base/smalltalk.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/sql.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/super-collider.css +33 -0
- data/public/_components/@socketry/syntax/themes/base/swift.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/syntax.css +63 -0
- data/public/_components/@socketry/syntax/themes/base/xml.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/xrb.css +29 -0
- data/public/_components/@socketry/syntax/themes/base/yaml.css +1 -0
- data/public/_components/@socketry/syntax/themes/theming.md +233 -0
- data/public/_components/@socketry/syntax/update-examples.js +135 -0
- data/public/_static/index.css +593 -0
- data/public/application.js +147 -0
- data/readme.md +69 -0
- data/releases.md +3 -0
- data/templates/code.xrb +12 -0
- data/templates/default.xrb +5 -0
- data/templates/image.xrb +8 -0
- data/templates/section.xrb +5 -0
- data/templates/title.xrb +8 -0
- data/templates/translation.xrb +8 -0
- data/templates/two_column.xrb +8 -0
- metadata +280 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "live"
|
|
7
|
+
require_relative "slide_view"
|
|
8
|
+
|
|
9
|
+
module Presently
|
|
10
|
+
# The presenter-facing view with notes, timing, and slide previews.
|
|
11
|
+
#
|
|
12
|
+
# Shows the current slide, next slide preview, presenter notes, timing controls,
|
|
13
|
+
# and pacing indicators. Updates the timing display every second via a background task.
|
|
14
|
+
class PresenterView < Live::View
|
|
15
|
+
# Initialize a new presenter view.
|
|
16
|
+
# @parameter id [String] The unique element identifier.
|
|
17
|
+
# @parameter data [Hash] The element data attributes.
|
|
18
|
+
# @parameter controller [PresentationController | Nil] The shared presentation controller.
|
|
19
|
+
def initialize(id = Live::Element.unique_id, data = {}, controller: nil)
|
|
20
|
+
super(id, data)
|
|
21
|
+
@controller = controller
|
|
22
|
+
@clock_task = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Bind this view to a page and start the timing update loop.
|
|
26
|
+
# @parameter page [Live::Page] The page this view is bound to.
|
|
27
|
+
def bind(page)
|
|
28
|
+
super
|
|
29
|
+
@controller.add_listener(self)
|
|
30
|
+
|
|
31
|
+
# Update only the timing section every second.
|
|
32
|
+
@clock_task = Async do
|
|
33
|
+
while true
|
|
34
|
+
update_timing!
|
|
35
|
+
sleep 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Close this view and stop the timing update loop.
|
|
41
|
+
def close
|
|
42
|
+
@clock_task&.stop
|
|
43
|
+
@controller.remove_listener(self)
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Called by the controller when the slide changes.
|
|
48
|
+
def slide_changed!
|
|
49
|
+
self.update!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Push an update to just the timing section.
|
|
53
|
+
def update_timing!
|
|
54
|
+
replace(".timing") do |builder|
|
|
55
|
+
render_timing(builder, @controller.current_slide)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Handle an event from the client.
|
|
60
|
+
# @parameter event [Hash] The event data with `:detail` containing the action.
|
|
61
|
+
def handle(event)
|
|
62
|
+
action = event.dig(:detail, :action)
|
|
63
|
+
|
|
64
|
+
case action
|
|
65
|
+
when "next"
|
|
66
|
+
@controller.advance!
|
|
67
|
+
when "previous"
|
|
68
|
+
@controller.retreat!
|
|
69
|
+
when "pause"
|
|
70
|
+
if !@controller.clock.started?
|
|
71
|
+
@controller.clock.start!
|
|
72
|
+
elsif @controller.clock.paused?
|
|
73
|
+
@controller.clock.resume!
|
|
74
|
+
else
|
|
75
|
+
@controller.clock.pause!
|
|
76
|
+
end
|
|
77
|
+
@controller.save_state!
|
|
78
|
+
when "reset"
|
|
79
|
+
@controller.reset_timer!
|
|
80
|
+
when "reload"
|
|
81
|
+
@controller.reload!
|
|
82
|
+
when "jump"
|
|
83
|
+
if index = event.dig(:detail, :index)
|
|
84
|
+
@controller.go_to(index.to_i)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Render the timing bar with controls, elapsed/remaining time, and pacing.
|
|
90
|
+
# @parameter builder [XRB::Builder] The HTML builder.
|
|
91
|
+
# @parameter slide [Slide | Nil] The current slide.
|
|
92
|
+
def render_timing(builder, slide)
|
|
93
|
+
progress = (@controller.slide_progress * 100).round(1)
|
|
94
|
+
builder.tag(:div, class: "timing", style: "--slide-progress: #{progress}%") do
|
|
95
|
+
pacing = @controller.pacing
|
|
96
|
+
pacing_class = case pacing
|
|
97
|
+
when :behind then "behind"
|
|
98
|
+
when :ahead then "ahead"
|
|
99
|
+
else "on-time"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
builder.tag(:div, class: "timing-info #{pacing_class}") do
|
|
103
|
+
builder.tag(:button,
|
|
104
|
+
class: "pause-button",
|
|
105
|
+
onClick: forward_event(action: "pause")
|
|
106
|
+
) do
|
|
107
|
+
label = if !@controller.clock.started?
|
|
108
|
+
"▶ Start"
|
|
109
|
+
elsif @controller.clock.paused?
|
|
110
|
+
"▶ Resume"
|
|
111
|
+
else
|
|
112
|
+
"⏸ Pause"
|
|
113
|
+
end
|
|
114
|
+
builder.text(label)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
builder.tag(:button,
|
|
118
|
+
class: "pause-button",
|
|
119
|
+
onClick: forward_event(action: "reset")
|
|
120
|
+
) do
|
|
121
|
+
builder.text("↺ Reset")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
builder.tag(:span, class: "elapsed") do
|
|
125
|
+
builder.text("Elapsed: #{format_duration(@controller.clock.elapsed)}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
builder.tag(:span, class: "remaining") do
|
|
129
|
+
builder.text("Remaining: #{format_duration(@controller.time_remaining)}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
builder.tag(:span, class: "pacing-indicator") do
|
|
133
|
+
indicator = case pacing
|
|
134
|
+
when :behind then "⏩ Speed up"
|
|
135
|
+
when :ahead then "⏪ Slow down"
|
|
136
|
+
else "✓ On time"
|
|
137
|
+
end
|
|
138
|
+
builder.text(indicator)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if slide
|
|
142
|
+
builder.tag(:span, class: "slide-duration") do
|
|
143
|
+
builder.text("Slide: #{format_duration(slide.duration)}")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Format a duration in seconds as `M:SS`.
|
|
151
|
+
# @parameter seconds [Numeric] The duration in seconds.
|
|
152
|
+
# @returns [String] The formatted duration string.
|
|
153
|
+
def format_duration(seconds)
|
|
154
|
+
seconds = seconds.to_i
|
|
155
|
+
minutes = seconds / 60
|
|
156
|
+
secs = seconds % 60
|
|
157
|
+
format("%d:%02d", minutes, secs)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Render the full presenter view.
|
|
161
|
+
# @parameter builder [XRB::Builder] The HTML builder.
|
|
162
|
+
def render(builder)
|
|
163
|
+
slide = @controller.current_slide
|
|
164
|
+
next_slide = @controller.next_slide
|
|
165
|
+
|
|
166
|
+
builder.tag(:div, class: "presenter") do
|
|
167
|
+
# Controls bar
|
|
168
|
+
builder.tag(:div, class: "controls") do
|
|
169
|
+
builder.tag(:button,
|
|
170
|
+
onClick: forward_event(action: "previous")
|
|
171
|
+
) do
|
|
172
|
+
builder.text("← Previous")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
builder.tag(:span, class: "slide-info") do
|
|
176
|
+
builder.text("Slide #{@controller.current_index + 1} of #{@controller.slide_count}")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
builder.tag(:button,
|
|
180
|
+
onClick: forward_event(action: "next")
|
|
181
|
+
) do
|
|
182
|
+
builder.text("Next →")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Jump-to dropdown for marked slides
|
|
186
|
+
markers = []
|
|
187
|
+
@controller.slides.each_with_index do |s, i|
|
|
188
|
+
if s.marker
|
|
189
|
+
markers << [i, s.marker]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
unless markers.empty?
|
|
194
|
+
builder.tag(:select,
|
|
195
|
+
class: "jump-to",
|
|
196
|
+
onChange: "live.forwardEvent(#{JSON.dump(@id)}, event, {action: 'jump', index: parseInt(this.value)}); this.value = '';"
|
|
197
|
+
) do
|
|
198
|
+
builder.tag(:option, value: "", disabled: true, selected: true) do
|
|
199
|
+
builder.text("Jump to…")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
markers.each do |index, label|
|
|
203
|
+
builder.tag(:option, value: index) do
|
|
204
|
+
builder.text(label)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
builder.tag(:button,
|
|
211
|
+
onClick: forward_event(action: "reload"),
|
|
212
|
+
class: "reload"
|
|
213
|
+
) do
|
|
214
|
+
builder.text("↻ Reload")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Slide previews
|
|
219
|
+
builder.tag(:div, class: "previews") do
|
|
220
|
+
# Current slide
|
|
221
|
+
builder.tag(:div, class: "preview current-preview") do
|
|
222
|
+
builder.tag(:h3){builder.text("Current")}
|
|
223
|
+
builder.tag(:div, class: "preview-frame") do
|
|
224
|
+
if slide
|
|
225
|
+
renderer = SlideView.new(css_class: "slide preview-slide", controller: @controller)
|
|
226
|
+
renderer.render_slide(builder, slide)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Next slide
|
|
232
|
+
builder.tag(:div, class: "preview next-preview") do
|
|
233
|
+
builder.tag(:h3){builder.text("Next")}
|
|
234
|
+
builder.tag(:div, class: "preview-frame") do
|
|
235
|
+
if next_slide
|
|
236
|
+
renderer = SlideView.new(css_class: "slide preview-slide", controller: @controller)
|
|
237
|
+
renderer.render_slide(builder, next_slide)
|
|
238
|
+
else
|
|
239
|
+
builder.tag(:div, class: "no-slide") do
|
|
240
|
+
builder.text("End of presentation")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Timing
|
|
248
|
+
render_timing(builder, slide)
|
|
249
|
+
|
|
250
|
+
# Presenter notes
|
|
251
|
+
builder.tag(:div, class: "notes") do
|
|
252
|
+
builder.tag(:h3){builder.text("Notes")}
|
|
253
|
+
builder.tag(:div, class: "notes-content") do
|
|
254
|
+
if slide&.notes
|
|
255
|
+
builder.raw(slide.notes)
|
|
256
|
+
else
|
|
257
|
+
builder.tag(:p, class: "no-notes"){builder.text("No presenter notes for this slide.")}
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "yaml"
|
|
7
|
+
require "markly"
|
|
8
|
+
|
|
9
|
+
module Presently
|
|
10
|
+
# A single slide parsed from a Markdown file.
|
|
11
|
+
#
|
|
12
|
+
# Each slide has YAML frontmatter for metadata (template, duration, focus), content sections
|
|
13
|
+
# split by Markdown headings, and optional presenter notes separated by `---`.
|
|
14
|
+
class Slide
|
|
15
|
+
# Initialize a new slide by parsing the given Markdown file.
|
|
16
|
+
# @parameter path [String] The file path to the Markdown slide.
|
|
17
|
+
def initialize(path)
|
|
18
|
+
@path = path
|
|
19
|
+
@frontmatter = nil
|
|
20
|
+
@content = nil
|
|
21
|
+
@notes = nil
|
|
22
|
+
|
|
23
|
+
parse!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @attribute [String] The file path of the slide.
|
|
27
|
+
attr :path
|
|
28
|
+
|
|
29
|
+
# @attribute [Hash | Nil] The parsed YAML frontmatter.
|
|
30
|
+
attr :frontmatter
|
|
31
|
+
|
|
32
|
+
# @attribute [Hash(String, String)] The content sections keyed by heading name.
|
|
33
|
+
attr :content
|
|
34
|
+
|
|
35
|
+
# @attribute [String | Nil] The rendered HTML presenter notes.
|
|
36
|
+
attr :notes
|
|
37
|
+
|
|
38
|
+
# The template to use for rendering this slide.
|
|
39
|
+
# @returns [String] The template name from frontmatter, or `"default"`.
|
|
40
|
+
def template
|
|
41
|
+
@frontmatter&.fetch("template", "default") || "default"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The expected duration of this slide in seconds.
|
|
45
|
+
# @returns [Integer] The duration from frontmatter, or `60`.
|
|
46
|
+
def duration
|
|
47
|
+
@frontmatter&.fetch("duration", 60) || 60
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The title of this slide.
|
|
51
|
+
# @returns [String] The title from frontmatter, or the filename without extension.
|
|
52
|
+
def title
|
|
53
|
+
@frontmatter&.fetch("title", File.basename(@path, ".md")) || File.basename(@path, ".md")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The navigation marker for this slide, used in the presenter's jump-to dropdown.
|
|
57
|
+
# @returns [String | Nil] The marker label, or `nil` if not marked.
|
|
58
|
+
def marker
|
|
59
|
+
@frontmatter&.fetch("marker", nil)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The transition type for animating into this slide.
|
|
63
|
+
# @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"magic-move"`), or `nil` for instant swap.
|
|
64
|
+
def transition
|
|
65
|
+
@frontmatter&.fetch("transition", nil)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The line range to focus on for code slides.
|
|
69
|
+
# @returns [Array(Integer, Integer) | Nil] The `[start, end]` line numbers (1-based), or `nil`.
|
|
70
|
+
def focus
|
|
71
|
+
if range = @frontmatter&.fetch("focus", nil)
|
|
72
|
+
parts = range.to_s.split("-").map(&:to_i)
|
|
73
|
+
parts.length == 2 ? parts : nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Parse the Markdown file into frontmatter, content sections, and notes.
|
|
80
|
+
def parse!
|
|
81
|
+
raw = File.read(@path)
|
|
82
|
+
|
|
83
|
+
# Extract YAML frontmatter:
|
|
84
|
+
if raw.start_with?("---\n")
|
|
85
|
+
parts = raw.split("---\n", 3)
|
|
86
|
+
if parts.length >= 3
|
|
87
|
+
@frontmatter = YAML.safe_load(parts[1])
|
|
88
|
+
body = parts[2]
|
|
89
|
+
else
|
|
90
|
+
body = raw
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
body = raw
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Split content and presenter notes (notes come after "---" on its own line):
|
|
97
|
+
if body.include?("\n---\n")
|
|
98
|
+
content_part, notes_part = body.split("\n---\n", 2)
|
|
99
|
+
@content = parse_sections(content_part.strip)
|
|
100
|
+
@notes = render_markdown(notes_part.strip)
|
|
101
|
+
else
|
|
102
|
+
@content = parse_sections(body.strip)
|
|
103
|
+
@notes = nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Parse content into sections based on Markdown headings.
|
|
108
|
+
# Each heading becomes a named key for the template.
|
|
109
|
+
# @parameter text [String] The Markdown content to parse.
|
|
110
|
+
# @returns [Hash(String, String)] Sections keyed by heading name, with rendered HTML values.
|
|
111
|
+
def parse_sections(text)
|
|
112
|
+
sections = {}
|
|
113
|
+
current_key = "body"
|
|
114
|
+
current_content = []
|
|
115
|
+
|
|
116
|
+
text.each_line do |line|
|
|
117
|
+
if line.match?(/\A#+\s+/)
|
|
118
|
+
# Save previous section:
|
|
119
|
+
unless current_content.empty?
|
|
120
|
+
sections[current_key] = render_markdown(current_content.join)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Extract heading text as the key:
|
|
124
|
+
heading_text = line.sub(/\A#+\s+/, "").strip.downcase.gsub(/\s+/, "_")
|
|
125
|
+
current_key = heading_text
|
|
126
|
+
current_content = []
|
|
127
|
+
else
|
|
128
|
+
current_content << line
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Save last section:
|
|
133
|
+
unless current_content.empty?
|
|
134
|
+
sections[current_key] = render_markdown(current_content.join)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
sections
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Render Markdown text to HTML.
|
|
141
|
+
# @parameter text [String] The Markdown text.
|
|
142
|
+
# @returns [String] The rendered HTML.
|
|
143
|
+
def render_markdown(text)
|
|
144
|
+
return "" if text.nil? || text.empty?
|
|
145
|
+
Markly.render_html(text, flags: Markly::UNSAFE)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "live"
|
|
7
|
+
require "xrb/template"
|
|
8
|
+
require "xrb/markup"
|
|
9
|
+
|
|
10
|
+
module Presently
|
|
11
|
+
# The default directory containing bundled slide templates.
|
|
12
|
+
DEFAULT_TEMPLATES_ROOT = File.expand_path("../../templates", __dir__)
|
|
13
|
+
|
|
14
|
+
# Renders a single slide using its XRB template.
|
|
15
|
+
#
|
|
16
|
+
# Loads templates from the configured templates root and renders slides
|
|
17
|
+
# by passing their content sections to the template via {TemplateScope}.
|
|
18
|
+
class SlideView < Live::View
|
|
19
|
+
# Initialize a new slide view.
|
|
20
|
+
# @parameter id [String] The unique element identifier.
|
|
21
|
+
# @parameter data [Hash] The element data attributes.
|
|
22
|
+
# @parameter css_class [String] The CSS class for the slide container.
|
|
23
|
+
# @parameter controller [PresentationController | Nil] The controller to get the templates root from.
|
|
24
|
+
def initialize(id = Live::Element.unique_id, data = {}, css_class: "slide", controller: nil)
|
|
25
|
+
super(id, data)
|
|
26
|
+
@css_class = css_class
|
|
27
|
+
@templates_root = controller&.templates_root || DEFAULT_TEMPLATES_ROOT
|
|
28
|
+
@templates = {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Load and cache a template by name.
|
|
32
|
+
# @parameter name [String] The template name (without extension).
|
|
33
|
+
# @returns [XRB::Template] The loaded template.
|
|
34
|
+
def template_for(name)
|
|
35
|
+
@templates[name] ||= begin
|
|
36
|
+
path = File.join(@templates_root, "#{name}.xrb")
|
|
37
|
+
XRB::Template.load_file(path)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Render a slide using its template into the builder.
|
|
42
|
+
# @parameter builder [XRB::Builder] The HTML builder.
|
|
43
|
+
# @parameter slide [Slide] The slide to render.
|
|
44
|
+
# @parameter extra_class [String | Nil] An additional CSS class for the container.
|
|
45
|
+
def render_slide(builder, slide, extra_class: nil)
|
|
46
|
+
return unless slide
|
|
47
|
+
|
|
48
|
+
template = template_for(slide.template)
|
|
49
|
+
scope = TemplateScope.new(slide)
|
|
50
|
+
html = template.to_string(scope)
|
|
51
|
+
|
|
52
|
+
classes = [@css_class, extra_class].compact.join(" ")
|
|
53
|
+
builder.tag(:div, class: classes, data: {template: slide.template}) do
|
|
54
|
+
builder.raw(html)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Render the current slide.
|
|
59
|
+
# @parameter builder [XRB::Builder] The HTML builder.
|
|
60
|
+
def render(builder)
|
|
61
|
+
slide = nil
|
|
62
|
+
render_slide(builder, slide)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Provides the scope for XRB template rendering.
|
|
67
|
+
#
|
|
68
|
+
# Templates access slide content via `self.section(name)` and slide metadata via `self.slide`.
|
|
69
|
+
class TemplateScope
|
|
70
|
+
# Initialize a new template scope for the given slide.
|
|
71
|
+
# @parameter slide [Slide] The slide being rendered.
|
|
72
|
+
def initialize(slide)
|
|
73
|
+
@slide = slide
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @attribute [Slide] The slide being rendered.
|
|
77
|
+
attr :slide
|
|
78
|
+
|
|
79
|
+
# The content sections of the slide.
|
|
80
|
+
# @returns [Hash(String, String)] Sections keyed by heading name.
|
|
81
|
+
def content
|
|
82
|
+
@slide.content
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get a named content section as raw HTML markup.
|
|
86
|
+
# @parameter name [String] The section name (derived from the Markdown heading).
|
|
87
|
+
# @returns [XRB::MarkupString] The rendered HTML content, safe for embedding.
|
|
88
|
+
def section(name)
|
|
89
|
+
XRB::MarkupString.raw(@slide.content[name] || "")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
module Presently
|
|
9
|
+
# Persists and restores presentation controller state to/from a JSON file.
|
|
10
|
+
#
|
|
11
|
+
# Tracks the current slide index, clock elapsed time, and clock running state.
|
|
12
|
+
# This allows the presentation to survive server restarts without losing position.
|
|
13
|
+
class State
|
|
14
|
+
# The default state file path.
|
|
15
|
+
DEFAULT_PATH = ".presently.json"
|
|
16
|
+
|
|
17
|
+
# Initialize a new state instance.
|
|
18
|
+
# @parameter path [String] The file path for the state file.
|
|
19
|
+
def initialize(path = DEFAULT_PATH)
|
|
20
|
+
@path = path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @attribute [String] The file path for the state file.
|
|
24
|
+
attr :path
|
|
25
|
+
|
|
26
|
+
# Save the controller's current state to disk.
|
|
27
|
+
# @parameter controller [PresentationController] The controller to save.
|
|
28
|
+
def save(controller)
|
|
29
|
+
data = {
|
|
30
|
+
current_index: controller.current_index,
|
|
31
|
+
elapsed: controller.clock.elapsed,
|
|
32
|
+
running: controller.clock.running?,
|
|
33
|
+
started: controller.clock.started?,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
File.write(@path, JSON.pretty_generate(data))
|
|
37
|
+
rescue => error
|
|
38
|
+
Console.warn(self, "Failed to save state", error: error)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Restore state into the given controller.
|
|
42
|
+
# @parameter controller [PresentationController] The controller to restore into.
|
|
43
|
+
def load(controller)
|
|
44
|
+
return unless File.exist?(@path)
|
|
45
|
+
|
|
46
|
+
data = JSON.parse(File.read(@path), symbolize_names: true)
|
|
47
|
+
|
|
48
|
+
# Restore slide position:
|
|
49
|
+
if index = data[:current_index]
|
|
50
|
+
controller.go_to(index.to_i)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Restore clock state:
|
|
54
|
+
if data[:started]
|
|
55
|
+
controller.clock.start!
|
|
56
|
+
controller.clock.reset!(data[:elapsed].to_f)
|
|
57
|
+
|
|
58
|
+
unless data[:running]
|
|
59
|
+
controller.clock.pause!
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
rescue => error
|
|
63
|
+
Console.warn(self, "Failed to load state", error: error)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/presently.rb
ADDED
data/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright, 2026, by Samuel Williams.
|
|
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.
|