presently 0.14.1 → 0.16.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
- checksums.yaml.gz.sig +0 -0
- data/bake/presently/export.rb +6 -3
- data/bake/presently/rehearse.rb +8 -8
- data/bake/presently/slides.rb +4 -3
- data/context/animating-slides.md +13 -25
- data/context/getting-started.md +21 -4
- data/context/index.yaml +1 -1
- data/lib/presently/application.rb +2 -2
- data/lib/presently/export.rb +9 -1
- data/lib/presently/presentation.rb +30 -26
- data/lib/presently/presenter_view.rb +2 -2
- data/lib/presently/slide.rb +26 -12
- data/lib/presently/version.rb +1 -1
- data/public/_static/index.css +0 -19
- data/public/slide.js +0 -19
- data/readme.md +12 -9
- data/releases.md +11 -0
- data.tar.gz.sig +0 -0
- metadata +4 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c2eaa843e93a1ef3f3c97b0e89c80a3ee4484ddbee4c105ca39bc7c324671ea
|
|
4
|
+
data.tar.gz: bc27aa664ca531b38da8b807e3fcb438a41c4338a180c1ddd39fc1daa0c549ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98959ac207c29fa8a4bf444e0d1e476e584183615d04abed09c8f74cda3a338ed61f9517f67d5e7e5bb40aa624c352100bf1c2f36c61663a81f61ec509fe52d8
|
|
7
|
+
data.tar.gz: 30ca2a30e72d1f0c0dbc71b31786349ecd8d8c70eae5d69f29e701e77cd2f39487909a71a4699b2be9b70169cc170065313da35e46394ab92d0b378f3849e38a
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/bake/presently/export.rb
CHANGED
|
@@ -50,12 +50,15 @@ def pdf(output: "presentation.pdf", slides_root: "slides", notes: true, speaker:
|
|
|
50
50
|
begin
|
|
51
51
|
environment = Async::Service::Environment.build(
|
|
52
52
|
Presently::Environment::Application,
|
|
53
|
-
root:
|
|
54
|
-
slides_root:
|
|
53
|
+
root: Dir.pwd,
|
|
54
|
+
slides_root: slides_root,
|
|
55
55
|
endpoint: Async::HTTP::Endpoint.parse("http://localhost", bound_endpoint)
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
# PDF export always requires an HTTP server, regardless of the configured
|
|
59
|
+
# Lively transport:
|
|
60
|
+
transport = environment.with(environment.evaluator.http_environment)
|
|
61
|
+
evaluator = transport.evaluator
|
|
59
62
|
server = evaluator.make_server(evaluator.endpoint)
|
|
60
63
|
server_task = task.async{server.run}
|
|
61
64
|
|
data/bake/presently/rehearse.rb
CHANGED
|
@@ -21,8 +21,8 @@ end
|
|
|
21
21
|
#
|
|
22
22
|
# @parameter slides_root [String] The slides directory. Default: `slides`.
|
|
23
23
|
# @parameter speaker [String | Nil] Only rehearse slides for this speaker.
|
|
24
|
-
# @parameter from [String | Nil] Substring match — start at the first slide whose
|
|
25
|
-
# @parameter to [String | Nil] Substring match — stop after the first slide whose
|
|
24
|
+
# @parameter from [String | Nil] Substring match — start at the first slide whose relative path contains this.
|
|
25
|
+
# @parameter to [String | Nil] Substring match — stop after the first slide whose relative path contains this.
|
|
26
26
|
# @parameter resume [Boolean] Resume from the slide after the last one recorded in the log. Keeps prior records and appends new ones.
|
|
27
27
|
# @parameter log [String] Path to write the JSON log. Default: `tmp/rehearsal.json`.
|
|
28
28
|
def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: false, log: "tmp/rehearsal.json")
|
|
@@ -31,12 +31,12 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
|
|
|
31
31
|
slides = slides.select{|slide| slide.speaker == speaker} if speaker
|
|
32
32
|
|
|
33
33
|
if from
|
|
34
|
-
start_index = slides.index{|slide|
|
|
34
|
+
start_index = slides.index{|slide| slide.path.include?(from)}
|
|
35
35
|
slides = slides[start_index..] if start_index
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
if to
|
|
39
|
-
end_index = slides.index{|slide|
|
|
39
|
+
end_index = slides.index{|slide| slide.path.include?(to)}
|
|
40
40
|
slides = slides[..end_index] if end_index
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -49,7 +49,7 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
|
|
|
49
49
|
|
|
50
50
|
prior_records = JSON.parse(File.read(log), symbolize_names: true)
|
|
51
51
|
last_path = prior_records.last&.dig(:path)
|
|
52
|
-
resume_index = slides.index{|slide|
|
|
52
|
+
resume_index = slides.index{|slide| slide.path == last_path}
|
|
53
53
|
|
|
54
54
|
if resume_index.nil?
|
|
55
55
|
puts "Last rehearsed slide (#{last_path}) not found in current slide set. Nothing to resume."
|
|
@@ -81,14 +81,14 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
|
|
|
81
81
|
records = []
|
|
82
82
|
offset = prior_records.length
|
|
83
83
|
slides.each_with_index do |slide, index|
|
|
84
|
-
|
|
84
|
+
relative_path = slide.path
|
|
85
85
|
headline = extract_headline(slide)
|
|
86
86
|
absolute = offset + index + 1
|
|
87
87
|
total = offset + slides.length
|
|
88
88
|
|
|
89
89
|
puts
|
|
90
90
|
puts "─" * 72
|
|
91
|
-
puts "[#{absolute}/#{total}] #{
|
|
91
|
+
puts "[#{absolute}/#{total}] #{relative_path} planned: #{format_duration(slide.duration)} speaker: #{slide.speaker || "—"}"
|
|
92
92
|
puts " #{headline}" unless headline.empty?
|
|
93
93
|
if slide.notes
|
|
94
94
|
notes = slide.notes.to_commonmark.strip
|
|
@@ -105,7 +105,7 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
|
|
|
105
105
|
|
|
106
106
|
records << {
|
|
107
107
|
index: absolute,
|
|
108
|
-
path:
|
|
108
|
+
path: relative_path,
|
|
109
109
|
speaker: slide.speaker,
|
|
110
110
|
planned: slide.duration,
|
|
111
111
|
actual: elapsed.round(1),
|
data/bake/presently/slides.rb
CHANGED
|
@@ -58,7 +58,7 @@ def speakers(slides_root: "slides")
|
|
|
58
58
|
puts "#{speaker} — #{format_duration(total_seconds)}"
|
|
59
59
|
|
|
60
60
|
slides.each do |slide|
|
|
61
|
-
puts " #{format_duration(slide.duration)} #{slide.title} (#{
|
|
61
|
+
puts " #{format_duration(slide.duration)} #{slide.title} (#{slide.path})"
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
puts
|
|
@@ -69,9 +69,10 @@ end
|
|
|
69
69
|
|
|
70
70
|
# Renumber slide files sequentially with a consistent step size.
|
|
71
71
|
#
|
|
72
|
-
# Renames all `.md` files in the
|
|
72
|
+
# Renames all `.md` files directly in the given directory to have sequential
|
|
73
73
|
# numeric prefixes (010, 020, 030, ...), preserving their current order.
|
|
74
|
-
# The descriptive part of the filename (after the number prefix) is kept.
|
|
74
|
+
# The descriptive part of the filename (after the number prefix) is kept. It
|
|
75
|
+
# does not recurse; pass a nested directory as `slides_root` to renumber it.
|
|
75
76
|
#
|
|
76
77
|
# @parameter slides_root [String] The slides directory. Default: `slides`.
|
|
77
78
|
# @parameter step [Integer] The step between slide numbers. Default: `10`.
|
data/context/animating-slides.md
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
# Animating Slides
|
|
2
2
|
|
|
3
|
-
This guide explains how to animate content within slides using the
|
|
4
|
-
|
|
5
|
-
## How Morph Works
|
|
6
|
-
|
|
7
|
-
The `morph` transition uses the browser's View Transitions API. When navigating between two slides, any element that has a `view-transition-name` style on both the old and new slide is matched — the browser captures its position and appearance in both states and animates between them.
|
|
8
|
-
|
|
9
|
-
Elements without a matching name crossfade. The slide background stays completely still (no container animation).
|
|
10
|
-
|
|
11
|
-
This is the mechanism that makes build sequences possible: the same element appears on consecutive slides with the same name, so it stays pinned in place while hidden elements appear around it.
|
|
3
|
+
This guide explains how to animate content within slides using the slide scripting system.
|
|
12
4
|
|
|
13
5
|
## Slide Scripts
|
|
14
6
|
|
|
@@ -18,7 +10,6 @@ Any slide can include a JavaScript block at the end of its presenter notes secti
|
|
|
18
10
|
---
|
|
19
11
|
template: default
|
|
20
12
|
duration: 30
|
|
21
|
-
transition: morph
|
|
22
13
|
---
|
|
23
14
|
|
|
24
15
|
- First point
|
|
@@ -30,7 +21,7 @@ transition: morph
|
|
|
30
21
|
Your presenter notes here.
|
|
31
22
|
|
|
32
23
|
```javascript
|
|
33
|
-
slide.find("li").show(1
|
|
24
|
+
slide.find("li").show(1)
|
|
34
25
|
```
|
|
35
26
|
~~~
|
|
36
27
|
|
|
@@ -52,7 +43,7 @@ slide.find(".callout") // elements with a specific class
|
|
|
52
43
|
|
|
53
44
|
### `elements.show(n, options)`
|
|
54
45
|
|
|
55
|
-
Shows the first `n` elements in the collection and hides the rest.
|
|
46
|
+
Shows the first `n` elements in the collection and hides the rest. Returns a `Promise` that resolves when any reveal animation completes.
|
|
56
47
|
|
|
57
48
|
``` javascript
|
|
58
49
|
slide.find("li").show(0) // all hidden
|
|
@@ -64,7 +55,6 @@ Options:
|
|
|
64
55
|
|
|
65
56
|
| Option | Description |
|
|
66
57
|
|---|---|
|
|
67
|
-
| `group` | Name prefix for `view-transition-name` — must be consistent across slides for morph to match elements. Defaults to `"build"`. |
|
|
68
58
|
| `effect` | Entry animation for the newly revealed element. See effects below. |
|
|
69
59
|
|
|
70
60
|
### `elements.builder(options)`
|
|
@@ -72,7 +62,7 @@ Options:
|
|
|
72
62
|
Creates a `SlideBuilder` with default options and a cached position. Use this instead of calling `show()` manually when you want to reveal elements one at a time from a script.
|
|
73
63
|
|
|
74
64
|
``` javascript
|
|
75
|
-
const bullets = slide.find("li").builder({
|
|
65
|
+
const bullets = slide.find("li").builder({effect: "fly-up"})
|
|
76
66
|
bullets.show(0) // hide all initially
|
|
77
67
|
bullets.next() // reveal first, plays fly-up
|
|
78
68
|
bullets.next() // reveal second, plays fly-up
|
|
@@ -115,7 +105,7 @@ A build sequence is a series of consecutive slides with the same content, each r
|
|
|
115
105
|
---
|
|
116
106
|
template: default
|
|
117
107
|
duration: 20
|
|
118
|
-
transition:
|
|
108
|
+
transition: fade
|
|
119
109
|
---
|
|
120
110
|
|
|
121
111
|
- Real-time synchronization
|
|
@@ -127,7 +117,7 @@ transition: morph
|
|
|
127
117
|
Let's walk through the key features.
|
|
128
118
|
|
|
129
119
|
```javascript
|
|
130
|
-
slide.find("li").show(0
|
|
120
|
+
slide.find("li").show(0)
|
|
131
121
|
```
|
|
132
122
|
~~~
|
|
133
123
|
|
|
@@ -136,7 +126,7 @@ slide.find("li").show(0, {group: "bullet"})
|
|
|
136
126
|
---
|
|
137
127
|
template: default
|
|
138
128
|
duration: 20
|
|
139
|
-
transition:
|
|
129
|
+
transition: fade
|
|
140
130
|
---
|
|
141
131
|
|
|
142
132
|
- Real-time synchronization
|
|
@@ -148,12 +138,10 @@ transition: morph
|
|
|
148
138
|
The display and presenter stay in sync over a WebSocket connection.
|
|
149
139
|
|
|
150
140
|
```javascript
|
|
151
|
-
slide.find("li").show(1
|
|
141
|
+
slide.find("li").show(1)
|
|
152
142
|
```
|
|
153
143
|
~~~
|
|
154
144
|
|
|
155
|
-
The `group` option must be identical across all slides in the sequence so the browser matches the same elements. Without it, each slide uses the default `"build"` prefix — which is fine as long as only one build sequence is active per slide.
|
|
156
|
-
|
|
157
145
|
Because all elements are in the DOM from the start (just hidden), the vertical layout stays consistent throughout the sequence — there is no shift as elements appear.
|
|
158
146
|
|
|
159
147
|
## Build Effects
|
|
@@ -161,7 +149,7 @@ Because all elements are in the DOM from the start (just hidden), the vertical l
|
|
|
161
149
|
Pass an `effect` option to animate the newly revealed element as it appears. The effect plays as a CSS animation on the element and is removed automatically once it completes.
|
|
162
150
|
|
|
163
151
|
``` javascript
|
|
164
|
-
slide.find("li").show(2, {
|
|
152
|
+
slide.find("li").show(2, {effect: "fly-up"})
|
|
165
153
|
```
|
|
166
154
|
|
|
167
155
|
Available effects:
|
|
@@ -181,8 +169,8 @@ A slide can have multiple independent build groups. Each `find().show()` call is
|
|
|
181
169
|
|
|
182
170
|
``` javascript
|
|
183
171
|
// Reveal list items as one group, callout div as another
|
|
184
|
-
slide.find("li").show(3
|
|
185
|
-
slide.find(".callout").show(1, {
|
|
172
|
+
slide.find("li").show(3)
|
|
173
|
+
slide.find(".callout").show(1, {effect: "fly-up"})
|
|
186
174
|
```
|
|
187
175
|
|
|
188
176
|
## In-Slide Animation with `slide.after()`
|
|
@@ -190,8 +178,8 @@ slide.find(".callout").show(1, {group: "callout", effect: "fly-up"})
|
|
|
190
178
|
For sequential reveals within a single slide (without navigating to the next slide), use `slide.after()`. Each step fires a delay in milliseconds relative to the previous step. Returns a `SlideContext` so subsequent `.after()` calls chain naturally.
|
|
191
179
|
|
|
192
180
|
``` javascript
|
|
193
|
-
const panes = slide.find(".pane").builder({
|
|
194
|
-
const items = slide.find(".item").builder({
|
|
181
|
+
const panes = slide.find(".pane").builder({effect: "fade"})
|
|
182
|
+
const items = slide.find(".item").builder({effect: "fly-up"})
|
|
195
183
|
panes.show(0)
|
|
196
184
|
items.show(0)
|
|
197
185
|
|
data/context/getting-started.md
CHANGED
|
@@ -32,7 +32,27 @@ $ mkdir slides
|
|
|
32
32
|
|
|
33
33
|
### Writing Slides
|
|
34
34
|
|
|
35
|
-
Each slide is a Markdown file in the `slides/` directory. Files are ordered
|
|
35
|
+
Each slide is a Markdown file in the `slides/` directory. Files are ordered by path, and every directory and slide filename must begin with a numeric prefix:
|
|
36
|
+
|
|
37
|
+
``` text
|
|
38
|
+
slides/
|
|
39
|
+
├── 010-welcome.md
|
|
40
|
+
├── 020-performance/
|
|
41
|
+
│ ├── 010-introduction.md
|
|
42
|
+
│ └── 020-results.md
|
|
43
|
+
└── 030-conclusion.md
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This allows related slides to be grouped in nested directories while retaining an unambiguous presentation order. Markdown files with any unnumbered path component, such as `slides/shared/example.md`, are not treated as slides and can be used for included content.
|
|
47
|
+
|
|
48
|
+
The `presently:slides:renumber` task only renumbers files directly inside the selected directory. Run it without arguments for the top-level slides, or pass a nested directory explicitly:
|
|
49
|
+
|
|
50
|
+
``` shell
|
|
51
|
+
$ bake presently:slides:renumber
|
|
52
|
+
$ bake presently:slides:renumber slides_root=slides/020-performance
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A slide file contains Markdown with optional frontmatter and presenter notes:
|
|
36
56
|
|
|
37
57
|
``` markdown
|
|
38
58
|
---
|
|
@@ -263,9 +283,6 @@ Available transitions:
|
|
|
263
283
|
| `fade` | Crossfade between slides |
|
|
264
284
|
| `slide-left` | Current slide exits left, next enters from right |
|
|
265
285
|
| `slide-right` | Current slide exits right, next enters from left |
|
|
266
|
-
| `morph` | Matched elements animate between positions; everything else crossfades |
|
|
267
|
-
|
|
268
|
-
The `morph` transition uses the browser's View Transitions API to smoothly animate individual elements between slides. Elements with the same `view-transition-name` across two consecutive slides are matched and interpolated.
|
|
269
286
|
|
|
270
287
|
## Presenter Notes
|
|
271
288
|
|
data/context/index.yaml
CHANGED
|
@@ -49,7 +49,7 @@ module Presently
|
|
|
49
49
|
def controller
|
|
50
50
|
@controller ||= begin
|
|
51
51
|
templates = Templates.for(@templates_roots)
|
|
52
|
-
presentation = Presentation.load(@slides_root, templates
|
|
52
|
+
presentation = Presentation.load(@slides_root, templates)
|
|
53
53
|
|
|
54
54
|
PresentationController.new(presentation, state: State.new)
|
|
55
55
|
end
|
|
@@ -81,7 +81,7 @@ module Presently
|
|
|
81
81
|
|
|
82
82
|
if path == "/export"
|
|
83
83
|
options = Export.options_from_query(query)
|
|
84
|
-
presentation = Presentation.load(@slides_root,
|
|
84
|
+
presentation = Presentation.load(@slides_root, controller.templates)
|
|
85
85
|
export = Export.new(presentation: presentation, **options)
|
|
86
86
|
return Protocol::HTTP::Response[200, [["content-type", "text/html"]], [export.call]]
|
|
87
87
|
end
|
data/lib/presently/export.rb
CHANGED
|
@@ -20,8 +20,16 @@ module Presently
|
|
|
20
20
|
PageSize = Struct.new(:slide_width_px, :slide_height_px, :notes_height_px, keyword_init: true) do
|
|
21
21
|
PX_PER_CM = 96.0 / 2.54
|
|
22
22
|
|
|
23
|
+
# Convert the slide width from CSS pixels to centimetres.
|
|
24
|
+
# @returns [Float] The slide width in centimetres.
|
|
23
25
|
def slide_width_cm = (slide_width_px / PX_PER_CM).round(4)
|
|
26
|
+
|
|
27
|
+
# Convert the slide height from CSS pixels to centimetres.
|
|
28
|
+
# @returns [Float] The slide height in centimetres.
|
|
24
29
|
def slide_height_cm = (slide_height_px / PX_PER_CM).round(4)
|
|
30
|
+
|
|
31
|
+
# Convert the notes panel height from CSS pixels to centimetres.
|
|
32
|
+
# @returns [Float] The notes panel height in centimetres.
|
|
25
33
|
def notes_height_cm = (notes_height_px / PX_PER_CM).round(4)
|
|
26
34
|
end
|
|
27
35
|
|
|
@@ -98,7 +106,7 @@ module Presently
|
|
|
98
106
|
end
|
|
99
107
|
|
|
100
108
|
builder.tag(:span, class: "export-filename") do
|
|
101
|
-
builder.tag(:code){builder.text(
|
|
109
|
+
builder.tag(:code){builder.text(slide.path)}
|
|
102
110
|
end
|
|
103
111
|
|
|
104
112
|
if @timing
|
|
@@ -9,40 +9,33 @@ require_relative "templates"
|
|
|
9
9
|
module Presently
|
|
10
10
|
# An immutable collection of slides with configuration.
|
|
11
11
|
#
|
|
12
|
-
# Use {.load} to create a presentation from a directory of Markdown files
|
|
13
|
-
#
|
|
12
|
+
# Use {.load} to create a presentation from a directory of Markdown files.
|
|
13
|
+
# Each loaded {Slide} is attached to its presentation and identified by a
|
|
14
|
+
# path relative to the presentation root.
|
|
14
15
|
class Presentation
|
|
15
|
-
# Load and sort slide files from a directory.
|
|
16
|
-
# @parameter slides_root [String] The directory containing `.md` slide files.
|
|
17
|
-
# @returns [Array(Slide)] The loaded, sorted, non-skipped slides.
|
|
18
|
-
def self.slides_from(slides_root)
|
|
19
|
-
Dir.glob(File.join(slides_root, "*.md")).sort.map{|path| Slide.load(path)}.reject(&:skip?)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
16
|
# Load a presentation from a directory of Markdown slide files.
|
|
23
|
-
# @parameter
|
|
24
|
-
# @parameter
|
|
17
|
+
# @parameter root [String] The directory containing `.md` slide files.
|
|
18
|
+
# @parameter templates [Templates] The template resolver for loading slide templates.
|
|
25
19
|
# @returns [Presentation] A new presentation with slides loaded from the directory.
|
|
26
|
-
def self.load(
|
|
27
|
-
new(
|
|
20
|
+
def self.load(root = "slides", templates = Templates.for)
|
|
21
|
+
new(root, templates)
|
|
28
22
|
end
|
|
29
23
|
|
|
30
24
|
# Initialize a new presentation.
|
|
31
|
-
# @parameter
|
|
32
|
-
# @parameter slides_root [String | Nil] The directory slides were loaded from, used by {#reload}.
|
|
25
|
+
# @parameter root [String] The root directory containing slide files.
|
|
33
26
|
# @parameter templates [Templates] The template resolver for loading slide templates.
|
|
34
|
-
def initialize(
|
|
35
|
-
@
|
|
36
|
-
@slides_root = slides_root
|
|
27
|
+
def initialize(root = "slides", templates = Templates.for)
|
|
28
|
+
@root = File.expand_path(root)
|
|
37
29
|
@templates = templates
|
|
30
|
+
@slides = load_slides
|
|
38
31
|
end
|
|
39
32
|
|
|
33
|
+
# @attribute [String] The absolute root directory containing slide files.
|
|
34
|
+
attr :root
|
|
35
|
+
|
|
40
36
|
# @attribute [Array(Slide)] The ordered list of slides.
|
|
41
37
|
attr :slides
|
|
42
38
|
|
|
43
|
-
# @attribute [String | Nil] The directory slides were loaded from.
|
|
44
|
-
attr :slides_root
|
|
45
|
-
|
|
46
39
|
# @attribute [Templates] The template resolver.
|
|
47
40
|
attr :templates
|
|
48
41
|
|
|
@@ -66,12 +59,23 @@ module Presently
|
|
|
66
59
|
end
|
|
67
60
|
|
|
68
61
|
# Return a new {Presentation} with freshly loaded slides and a cleared template cache.
|
|
69
|
-
#
|
|
70
|
-
# @returns [Presentation] A new presentation instance, or `self` if no slides root is set.
|
|
62
|
+
# @returns [Presentation] A new presentation instance.
|
|
71
63
|
def reload
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
self.class.new(@root, @templates.reload)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Load slides recursively in relative path order.
|
|
70
|
+
# @returns [Array(Slide)] The loaded, sorted, non-skipped slides.
|
|
71
|
+
def load_slides
|
|
72
|
+
Dir.glob("**/*.md", base: @root).sort.filter_map do |path|
|
|
73
|
+
components = path.split(File::SEPARATOR)
|
|
74
|
+
next unless components.all?{|component| component.match?(/\A\d/)}
|
|
75
|
+
|
|
76
|
+
slide = Slide.load(self, path)
|
|
77
|
+
slide unless slide.skip?
|
|
78
|
+
end
|
|
75
79
|
end
|
|
76
80
|
end
|
|
77
81
|
end
|
|
@@ -204,10 +204,10 @@ module Presently
|
|
|
204
204
|
if slide
|
|
205
205
|
builder.text(" · ")
|
|
206
206
|
builder.tag(:code, class: "slide-path") do
|
|
207
|
-
builder.text(
|
|
207
|
+
builder.text(slide.path)
|
|
208
208
|
end
|
|
209
209
|
|
|
210
|
-
if editor_url = editor_url_for(slide.
|
|
210
|
+
if editor_url = editor_url_for(slide.source_path)
|
|
211
211
|
builder.tag(:a, href: editor_url, class: "edit-link") do
|
|
212
212
|
builder.text("✎")
|
|
213
213
|
end
|
data/lib/presently/slide.rb
CHANGED
|
@@ -61,15 +61,17 @@ module Presently
|
|
|
61
61
|
module_function
|
|
62
62
|
|
|
63
63
|
# Parse the file and return a {Slide}.
|
|
64
|
-
# @parameter
|
|
64
|
+
# @parameter presentation [Presentation] The presentation which owns the slide.
|
|
65
|
+
# @parameter path [String] The slide path relative to the presentation root.
|
|
65
66
|
# @returns [Slide]
|
|
66
|
-
def load(path)
|
|
67
|
-
|
|
67
|
+
def load(presentation, path)
|
|
68
|
+
source_path = File.join(presentation.root, path)
|
|
69
|
+
raw = File.read(source_path)
|
|
68
70
|
|
|
69
71
|
# Parse once, with native front matter support.
|
|
70
72
|
document = Markly.parse(raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER, extensions: Fragment::EXTENSIONS)
|
|
71
73
|
|
|
72
|
-
expand_includes!(document, File.dirname(
|
|
74
|
+
expand_includes!(document, File.dirname(source_path))
|
|
73
75
|
|
|
74
76
|
# Extract front matter from the first AST node if present.
|
|
75
77
|
front_matter = nil
|
|
@@ -111,7 +113,7 @@ module Presently
|
|
|
111
113
|
script = nil
|
|
112
114
|
end
|
|
113
115
|
|
|
114
|
-
Slide.new(path, front_matter: front_matter, content: content, notes: notes, script: script)
|
|
116
|
+
Slide.new(presentation, path, front_matter: front_matter, content: content, notes: notes, script: script)
|
|
115
117
|
end
|
|
116
118
|
|
|
117
119
|
# Expand `![[path/to/file.md]]` include directives in a parsed document.
|
|
@@ -183,19 +185,22 @@ module Presently
|
|
|
183
185
|
end
|
|
184
186
|
|
|
185
187
|
# Load and parse a slide from a Markdown file.
|
|
186
|
-
# @parameter
|
|
188
|
+
# @parameter presentation [Presentation] The presentation which owns the slide.
|
|
189
|
+
# @parameter path [String] The slide path relative to the presentation root.
|
|
187
190
|
# @returns [Slide]
|
|
188
|
-
def self.load(path)
|
|
189
|
-
Parser.load(path)
|
|
191
|
+
def self.load(presentation, path)
|
|
192
|
+
Parser.load(presentation, path)
|
|
190
193
|
end
|
|
191
194
|
|
|
192
195
|
# Initialize a slide with pre-parsed data.
|
|
193
|
-
# @parameter
|
|
196
|
+
# @parameter presentation [Presentation] The presentation which owns the slide.
|
|
197
|
+
# @parameter path [String] The slide path relative to the presentation root.
|
|
194
198
|
# @parameter front_matter [Hash | Nil] The parsed YAML front_matter.
|
|
195
199
|
# @parameter content [Hash(String, Fragment)] Content sections keyed by heading name.
|
|
196
200
|
# @parameter notes [Fragment | Nil] The presenter notes as a Markly AST fragment.
|
|
197
201
|
# @parameter script [String | Nil] JavaScript to execute after the slide renders.
|
|
198
|
-
def initialize(path, front_matter: nil, content: {}, notes: nil, script: nil)
|
|
202
|
+
def initialize(presentation, path, front_matter: nil, content: {}, notes: nil, script: nil)
|
|
203
|
+
@presentation = presentation
|
|
199
204
|
@path = path
|
|
200
205
|
@front_matter = front_matter
|
|
201
206
|
@content = content
|
|
@@ -203,9 +208,18 @@ module Presently
|
|
|
203
208
|
@script = script
|
|
204
209
|
end
|
|
205
210
|
|
|
206
|
-
# @attribute [
|
|
211
|
+
# @attribute [Presentation] The presentation which owns the slide.
|
|
212
|
+
attr :presentation
|
|
213
|
+
|
|
214
|
+
# @attribute [String] The slide path relative to the presentation root.
|
|
207
215
|
attr :path
|
|
208
216
|
|
|
217
|
+
# The absolute path of the slide source file.
|
|
218
|
+
# @returns [String]
|
|
219
|
+
def source_path
|
|
220
|
+
File.join(@presentation.root, @path)
|
|
221
|
+
end
|
|
222
|
+
|
|
209
223
|
# @attribute [Hash | Nil] The parsed YAML front_matter.
|
|
210
224
|
attr :front_matter
|
|
211
225
|
|
|
@@ -249,7 +263,7 @@ module Presently
|
|
|
249
263
|
end
|
|
250
264
|
|
|
251
265
|
# The transition type for animating into this slide.
|
|
252
|
-
# @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"
|
|
266
|
+
# @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"slide-right"`), or `nil` for instant swap.
|
|
253
267
|
def transition
|
|
254
268
|
@front_matter&.fetch("transition", nil)
|
|
255
269
|
end
|
data/lib/presently/version.rb
CHANGED
data/public/_static/index.css
CHANGED
|
@@ -364,18 +364,6 @@ html[data-transition="slide-right"]::view-transition-new(slide-container) {
|
|
|
364
364
|
animation: vt-slide-in-left 0.4s ease-in-out;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
/* Magic move — the browser interpolates position/size for matched
|
|
368
|
-
view-transition-name elements. No cross-fade on the container
|
|
369
|
-
to avoid background dimming. */
|
|
370
|
-
html[data-transition="morph"]::view-transition-old(slide-container) {
|
|
371
|
-
animation: none;
|
|
372
|
-
opacity: 0;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
376
|
-
animation: none;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
367
|
@keyframes vt-fade-out {
|
|
380
368
|
from { opacity: 1; }
|
|
381
369
|
to { opacity: 0; }
|
|
@@ -410,13 +398,6 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
|
410
398
|
BUILD EFFECTS
|
|
411
399
|
======================== */
|
|
412
400
|
|
|
413
|
-
/* Suppress both pseudo-elements for hidden build elements so they
|
|
414
|
-
neither crossfade in nor crossfade out during the transition. */
|
|
415
|
-
::view-transition-old(.build-hidden),
|
|
416
|
-
::view-transition-new(.build-hidden) {
|
|
417
|
-
display: none;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
401
|
/* Fade */
|
|
421
402
|
.build-fade {
|
|
422
403
|
animation: vt-fade-in 0.4s ease;
|
data/public/slide.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// instead of tracking count manually. Created via SlideElements#builder(options).
|
|
4
4
|
export class SlideBuilder {
|
|
5
5
|
#elements;
|
|
6
|
-
#prefix;
|
|
7
6
|
#defaultEffect;
|
|
8
7
|
#slide;
|
|
9
8
|
#step = 0;
|
|
@@ -11,7 +10,6 @@ export class SlideBuilder {
|
|
|
11
10
|
constructor(slide, elements, options = {}) {
|
|
12
11
|
this.#slide = slide;
|
|
13
12
|
this.#elements = elements;
|
|
14
|
-
this.#prefix = options.group || 'build';
|
|
15
13
|
this.#defaultEffect = options.effect || null;
|
|
16
14
|
}
|
|
17
15
|
|
|
@@ -26,16 +24,8 @@ export class SlideBuilder {
|
|
|
26
24
|
let revealedElement = null;
|
|
27
25
|
|
|
28
26
|
this.#elements.forEach((element, index) => {
|
|
29
|
-
// Only assign a group name if the element doesn't already have an explicit one.
|
|
30
|
-
// Preserving explicit names allows elements to participate in morph transitions
|
|
31
|
-
// to other slides while still being managed by the build system.
|
|
32
|
-
if (!element.style.viewTransitionName || element.style.viewTransitionName === 'none') {
|
|
33
|
-
element.style.viewTransitionName = `${this.#prefix}-${index + 1}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
27
|
if (index < count) {
|
|
37
28
|
element.style.visibility = 'visible';
|
|
38
|
-
element.style.viewTransitionClass = '';
|
|
39
29
|
|
|
40
30
|
if (index === count - 1 && effect) {
|
|
41
31
|
element.classList.add(`build-${effect}`);
|
|
@@ -43,9 +33,6 @@ export class SlideBuilder {
|
|
|
43
33
|
}
|
|
44
34
|
} else {
|
|
45
35
|
element.style.visibility = 'hidden';
|
|
46
|
-
// Keep viewTransitionClass set so morph transitions can suppress
|
|
47
|
-
// crossfading on hidden elements when called inside startViewTransition.
|
|
48
|
-
element.style.viewTransitionClass = 'build-hidden';
|
|
49
36
|
}
|
|
50
37
|
});
|
|
51
38
|
|
|
@@ -75,11 +62,7 @@ export class SlideBuilder {
|
|
|
75
62
|
const effect = this.#slide.animated ? (overrides.effect !== undefined ? overrides.effect : this.#defaultEffect) : null;
|
|
76
63
|
const element = this.#elements[this.#step];
|
|
77
64
|
|
|
78
|
-
if (!element.style.viewTransitionName || element.style.viewTransitionName === 'none') {
|
|
79
|
-
element.style.viewTransitionName = `${this.#prefix}-${this.#step + 1}`;
|
|
80
|
-
}
|
|
81
65
|
element.style.visibility = 'visible';
|
|
82
|
-
element.style.viewTransitionClass = '';
|
|
83
66
|
|
|
84
67
|
this.#step += 1;
|
|
85
68
|
|
|
@@ -141,7 +124,6 @@ export class SlideElements {
|
|
|
141
124
|
|
|
142
125
|
// Create a stateful SlideBuilder for this element collection with default options.
|
|
143
126
|
// @parameter options [Object] Default options applied to every show() / next() call.
|
|
144
|
-
// group: prefix for view-transition-name (default: "build")
|
|
145
127
|
// effect: "fade", "fly-up", "fly-down", "fly-left", "fly-right", "scale"
|
|
146
128
|
// @returns [SlideBuilder]
|
|
147
129
|
builder(options = {}) {
|
|
@@ -152,7 +134,6 @@ export class SlideElements {
|
|
|
152
134
|
// Delegates to SlideBuilder for the actual implementation.
|
|
153
135
|
// @parameter count [Integer] Number of elements to show.
|
|
154
136
|
// @parameter options [Object]
|
|
155
|
-
// group: prefix for view-transition-name (default: "build")
|
|
156
137
|
// effect: "fade", "fly-up", "fly-down", "fly-left", "fly-right", "scale"
|
|
157
138
|
// @returns [Promise] Resolves when the animation completes (or immediately if no effect).
|
|
158
139
|
show(count, options = {}) {
|
data/readme.md
CHANGED
|
@@ -23,12 +23,23 @@ Please see the [project documentation](https://socketry.github.io/presently/) fo
|
|
|
23
23
|
|
|
24
24
|
- [Getting Started](https://socketry.github.io/presently/guides/getting-started/index) - This guide explains how to use `presently` to create and deliver web-based presentations using Markdown slides.
|
|
25
25
|
|
|
26
|
-
- [Animating Slides](https://socketry.github.io/presently/guides/animating-slides/index) - This guide explains how to animate content within slides using the
|
|
26
|
+
- [Animating Slides](https://socketry.github.io/presently/guides/animating-slides/index) - This guide explains how to animate content within slides using the slide scripting system.
|
|
27
27
|
|
|
28
28
|
## Releases
|
|
29
29
|
|
|
30
30
|
Please see the [project releases](https://socketry.github.io/presently/releases/index) for all releases.
|
|
31
31
|
|
|
32
|
+
### v0.16.0
|
|
33
|
+
|
|
34
|
+
- Add support for organizing slides in nested directories. Every directory and slide filename must begin with a numeric prefix, and slides are ordered by relative path.
|
|
35
|
+
|
|
36
|
+
### v0.15.0
|
|
37
|
+
|
|
38
|
+
q
|
|
39
|
+
|
|
40
|
+
- Export works from current working directory.
|
|
41
|
+
- Remove explicit support for `morph` transition.
|
|
42
|
+
|
|
32
43
|
### v0.14.0
|
|
33
44
|
|
|
34
45
|
- Increase code font size by 50%.
|
|
@@ -73,14 +84,6 @@ Please see the [project releases](https://socketry.github.io/presently/releases/
|
|
|
73
84
|
- Add `Slide#setTimeout(callback, delay)` — a tracked replacement for the global `setTimeout`. All timeouts registered this way are automatically cancelled when the slide changes, preventing stale callbacks from firing after navigation. The global `setTimeout` in slide scripts is shadowed by this method automatically.
|
|
74
85
|
- Add `Slide#cancelTimeouts()` — cancels all pending timeouts registered by the slide's script. Called automatically by the presentation engine on every slide change.
|
|
75
86
|
|
|
76
|
-
### v0.6.0
|
|
77
|
-
|
|
78
|
-
- Add `bake presently:slides:speakers` task to print a timing breakdown grouped by speaker. Each speaker's slides are listed in presentation order with individual and total durations, making it easy to balance talk time in multi-speaker presentations. Slides without a `speaker` key are grouped under `(no speaker)`.
|
|
79
|
-
|
|
80
|
-
### v0.5.0
|
|
81
|
-
|
|
82
|
-
- Add optional `speaker` front matter key to slides. When present, the current speaker's name is shown in the timing bar. If the next slide has a different speaker, a handoff indicator (e.g. `→ Next Speaker`) is shown alongside, giving presenters an at-a-glance cue for tag-team talks.
|
|
83
|
-
|
|
84
87
|
## See Also
|
|
85
88
|
|
|
86
89
|
- [lively](https://github.com/socketry/lively) — The real-time application framework that powers Presently.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.16.0
|
|
4
|
+
|
|
5
|
+
- Add support for organizing slides in nested directories. Every directory and slide filename must begin with a numeric prefix, and slides are ordered by relative path.
|
|
6
|
+
|
|
7
|
+
## v0.15.0
|
|
8
|
+
|
|
9
|
+
q
|
|
10
|
+
|
|
11
|
+
- Export works from current working directory.
|
|
12
|
+
- Remove explicit support for `morph` transition.
|
|
13
|
+
|
|
3
14
|
## v0.14.0
|
|
4
15
|
|
|
5
16
|
- Increase code font size by 50%.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: presently
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -44,14 +44,14 @@ dependencies:
|
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '0.
|
|
47
|
+
version: '0.18'
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '0.
|
|
54
|
+
version: '0.18'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: markly
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -243,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
243
243
|
- !ruby/object:Gem::Version
|
|
244
244
|
version: '0'
|
|
245
245
|
requirements: []
|
|
246
|
-
rubygems_version: 4.0.
|
|
246
|
+
rubygems_version: 4.0.10
|
|
247
247
|
specification_version: 4
|
|
248
248
|
summary: A web-based presentation tool built with Lively.
|
|
249
249
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|