presently 0.2.0 → 0.4.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 +5 -0
- data/bake/presently/slides.rb +24 -0
- data/lib/presently/presenter_view.rb +2 -2
- data/lib/presently/slide.rb +88 -39
- data/lib/presently/slide_renderer.rb +8 -2
- data/lib/presently/version.rb +1 -1
- data/public/_static/index.css +91 -2
- data/public/application.js +30 -3
- data/public/slide.js +52 -0
- data/readme.md +24 -3
- data/releases.md +17 -0
- data/templates/diagram.xrb +5 -0
- data.tar.gz.sig +0 -0
- metadata +34 -2
- 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: 3991c95331fdcabd96b64dd78833690a52468f2a1e570eddf3234fd5e9bfa7ac
|
|
4
|
+
data.tar.gz: 816e1de43c33929e12424659455e116891294d79dcc3d2ea41cc8df9b13317dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc5afae0aaeb288b51b4ccd6c8e66a562acd44898a49a3777299865ccff1a400258e88dd1fa3950071db8949b107fa076eac2d5042ccdd847e11d8d395801892
|
|
7
|
+
data.tar.gz: e0b1a9f6e5743beecdb4745469335c5ed8ad154e74bd5fdf2c745f54ce83532c1d81af3014c67ff8c9480e878b09a21e0df50cf73805e2a5676c0516a8ef807a
|
checksums.yaml.gz.sig
ADDED
data/bake/presently/slides.rb
CHANGED
|
@@ -9,6 +9,30 @@ def initialize(context)
|
|
|
9
9
|
require "fileutils"
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Extract all presenter notes and print them to stdout.
|
|
13
|
+
#
|
|
14
|
+
# Loads every slide in the slides directory using the Presently API and
|
|
15
|
+
# prints each slide's presenter notes to stdout. Each slide's notes are
|
|
16
|
+
# preceded by a `##` heading with the slide file path.
|
|
17
|
+
#
|
|
18
|
+
# @parameter slides_root [String] The slides directory. Default: `slides`.
|
|
19
|
+
def notes(slides_root: "slides")
|
|
20
|
+
require "presently"
|
|
21
|
+
|
|
22
|
+
presentation = Presently::Presentation.load(slides_root)
|
|
23
|
+
|
|
24
|
+
presentation.slides.each do |slide|
|
|
25
|
+
next unless slide.notes
|
|
26
|
+
|
|
27
|
+
puts "## #{slide.path}"
|
|
28
|
+
puts
|
|
29
|
+
puts slide.notes.to_commonmark
|
|
30
|
+
puts
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return nil
|
|
34
|
+
end
|
|
35
|
+
|
|
12
36
|
# Renumber slide files sequentially with a consistent step size.
|
|
13
37
|
#
|
|
14
38
|
# Renames all `.md` files in the slides directory to have sequential
|
|
@@ -270,8 +270,8 @@ module Presently
|
|
|
270
270
|
builder.tag(:div, class: "notes") do
|
|
271
271
|
builder.tag(:h3){builder.text("Notes")}
|
|
272
272
|
builder.tag(:div, class: "notes-content") do
|
|
273
|
-
if slide&.notes
|
|
274
|
-
builder.raw(
|
|
273
|
+
if notes = slide&.notes
|
|
274
|
+
builder.raw(notes.to_html)
|
|
275
275
|
else
|
|
276
276
|
builder.tag(:p, class: "no-notes"){builder.text("No presenter notes for this slide.")}
|
|
277
277
|
end
|
data/lib/presently/slide.rb
CHANGED
|
@@ -14,14 +14,52 @@ module Presently
|
|
|
14
14
|
# Each slide has YAML front_matter for metadata (template, duration, focus), content sections
|
|
15
15
|
# split by Markdown headings, and optional presenter notes separated by `---`.
|
|
16
16
|
class Slide
|
|
17
|
-
#
|
|
17
|
+
# A fragment of a Markly AST document.
|
|
18
18
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
19
|
+
# Wraps a `Markly::Node` of type `:document` and provides rendering helpers.
|
|
20
|
+
# Used for both content sections and presenter notes so callers can choose
|
|
21
|
+
# their output format without the parser pre-committing to one.
|
|
22
|
+
class Fragment
|
|
22
23
|
# Markly extensions enabled for all slide Markdown rendering.
|
|
23
24
|
EXTENSIONS = [:table, :tasklist, :strikethrough, :autolink]
|
|
24
25
|
|
|
26
|
+
# Initialize a fragment from a Markly document node.
|
|
27
|
+
# @parameter node [Markly::Node] A document node containing the fragment content.
|
|
28
|
+
def initialize(node)
|
|
29
|
+
@node = node
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @attribute [Markly::Node] The underlying AST document node.
|
|
33
|
+
attr :node
|
|
34
|
+
|
|
35
|
+
# Whether the fragment has no content.
|
|
36
|
+
# @returns [Boolean]
|
|
37
|
+
def empty?
|
|
38
|
+
@node.first_child.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Render the fragment to HTML using the Presently renderer.
|
|
42
|
+
#
|
|
43
|
+
# Mermaid fenced code blocks are rendered as `<mermaid-diagram>` elements.
|
|
44
|
+
# @returns [String] The rendered HTML.
|
|
45
|
+
def to_html
|
|
46
|
+
Renderer.new(flags: Markly::UNSAFE, extensions: EXTENSIONS).render(@node)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Render the fragment back to CommonMark Markdown.
|
|
50
|
+
# @returns [String] The CommonMark source.
|
|
51
|
+
def to_commonmark
|
|
52
|
+
@node.to_commonmark
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
alias to_s to_commonmark
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Parses a Markdown slide file into structured data for {Slide}.
|
|
59
|
+
#
|
|
60
|
+
# Handles YAML front_matter extraction, presenter note separation, and
|
|
61
|
+
# Markdown AST construction via Markly.
|
|
62
|
+
module Parser
|
|
25
63
|
module_function
|
|
26
64
|
|
|
27
65
|
# Parse the file and return a {Slide}.
|
|
@@ -31,7 +69,7 @@ module Presently
|
|
|
31
69
|
raw = File.read(path)
|
|
32
70
|
|
|
33
71
|
# Parse once, with native front matter support.
|
|
34
|
-
document = Markly.parse(raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER, extensions: EXTENSIONS)
|
|
72
|
+
document = Markly.parse(raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER, extensions: Fragment::EXTENSIONS)
|
|
35
73
|
|
|
36
74
|
# Extract front matter from the first AST node if present.
|
|
37
75
|
front_matter = nil
|
|
@@ -45,56 +83,62 @@ module Presently
|
|
|
45
83
|
document.each{|node| last_hrule = node if node.type == :hrule}
|
|
46
84
|
|
|
47
85
|
if last_hrule
|
|
48
|
-
|
|
86
|
+
notes_node = Markly::Node.new(:document)
|
|
49
87
|
while child = last_hrule.next
|
|
50
|
-
|
|
88
|
+
notes_node.append_child(child)
|
|
51
89
|
end
|
|
52
90
|
last_hrule.delete
|
|
53
91
|
|
|
54
|
-
|
|
55
|
-
|
|
92
|
+
# Extract the last javascript code block from the notes as the slide script.
|
|
93
|
+
script_node = nil
|
|
94
|
+
notes_node.each do |node|
|
|
95
|
+
if node.type == :code_block && node.fence_info.to_s.strip == "javascript"
|
|
96
|
+
script_node = node
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
script = nil
|
|
101
|
+
if script_node
|
|
102
|
+
script = script_node.string_content
|
|
103
|
+
script_node.delete
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
content = parse_sections(document)
|
|
107
|
+
notes = Fragment.new(notes_node)
|
|
56
108
|
else
|
|
57
|
-
content = parse_sections(document
|
|
109
|
+
content = parse_sections(document)
|
|
58
110
|
notes = nil
|
|
111
|
+
script = nil
|
|
59
112
|
end
|
|
60
113
|
|
|
61
|
-
Slide.new(path, front_matter: front_matter, content: content, notes: notes)
|
|
114
|
+
Slide.new(path, front_matter: front_matter, content: content, notes: notes, script: script)
|
|
62
115
|
end
|
|
63
116
|
|
|
64
|
-
# Parse a
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# @parameter
|
|
69
|
-
# @returns [Hash(String,
|
|
70
|
-
def parse_sections(
|
|
117
|
+
# Parse a Markly document into content sections based on top-level headings.
|
|
118
|
+
#
|
|
119
|
+
# Each heading becomes a named key; content before the first heading is
|
|
120
|
+
# collected under `"body"`. Each value is a {Fragment} wrapping a document node.
|
|
121
|
+
# @parameter document [Markly::Node] The document to parse.
|
|
122
|
+
# @returns [Hash(String, Fragment)] Sections keyed by heading name.
|
|
123
|
+
def parse_sections(document)
|
|
71
124
|
sections = {}
|
|
72
125
|
current_key = "body"
|
|
73
|
-
|
|
126
|
+
current_node = Markly::Node.new(:document)
|
|
74
127
|
|
|
75
|
-
|
|
128
|
+
document.each do |node|
|
|
76
129
|
if node.type == :header
|
|
77
|
-
sections[current_key] =
|
|
130
|
+
sections[current_key] = Fragment.new(current_node) unless current_node.first_child.nil?
|
|
78
131
|
current_key = node.to_plaintext.strip.downcase.gsub(/\s+/, "_")
|
|
79
|
-
|
|
132
|
+
current_node = Markly::Node.new(:document)
|
|
80
133
|
else
|
|
81
|
-
|
|
134
|
+
current_node.append_child(node.dup)
|
|
82
135
|
end
|
|
83
136
|
end
|
|
84
137
|
|
|
85
|
-
sections[current_key] =
|
|
138
|
+
sections[current_key] = Fragment.new(current_node) unless current_node.first_child.nil?
|
|
86
139
|
|
|
87
140
|
sections
|
|
88
141
|
end
|
|
89
|
-
|
|
90
|
-
# Render a list of AST nodes to HTML via a temporary document.
|
|
91
|
-
# @parameter nodes [Array(Markly::Node)] The nodes to render.
|
|
92
|
-
# @returns [String] The rendered HTML.
|
|
93
|
-
def render_nodes(nodes)
|
|
94
|
-
doc = Markly::Node.new(:document)
|
|
95
|
-
nodes.each{|node| doc.append_child(node.dup)}
|
|
96
|
-
Renderer.new(flags: Markly::UNSAFE, extensions: EXTENSIONS).render(doc)
|
|
97
|
-
end
|
|
98
142
|
end
|
|
99
143
|
|
|
100
144
|
# Load and parse a slide from a Markdown file.
|
|
@@ -107,13 +151,15 @@ module Presently
|
|
|
107
151
|
# Initialize a slide with pre-parsed data.
|
|
108
152
|
# @parameter path [String] The file path of the slide.
|
|
109
153
|
# @parameter front_matter [Hash | Nil] The parsed YAML front_matter.
|
|
110
|
-
# @parameter content [Hash(String,
|
|
111
|
-
# @parameter notes [
|
|
112
|
-
|
|
154
|
+
# @parameter content [Hash(String, Fragment)] Content sections keyed by heading name.
|
|
155
|
+
# @parameter notes [Fragment | Nil] The presenter notes as a Markly AST fragment.
|
|
156
|
+
# @parameter script [String | Nil] JavaScript to execute after the slide renders.
|
|
157
|
+
def initialize(path, front_matter: nil, content: {}, notes: nil, script: nil)
|
|
113
158
|
@path = path
|
|
114
159
|
@front_matter = front_matter
|
|
115
160
|
@content = content
|
|
116
161
|
@notes = notes
|
|
162
|
+
@script = script
|
|
117
163
|
end
|
|
118
164
|
|
|
119
165
|
# @attribute [String] The file path of the slide.
|
|
@@ -122,12 +168,15 @@ module Presently
|
|
|
122
168
|
# @attribute [Hash | Nil] The parsed YAML front_matter.
|
|
123
169
|
attr :front_matter
|
|
124
170
|
|
|
125
|
-
# @attribute [Hash(String,
|
|
171
|
+
# @attribute [Hash(String, Fragment)] The content sections keyed by heading name.
|
|
126
172
|
attr :content
|
|
127
173
|
|
|
128
|
-
# @attribute [
|
|
174
|
+
# @attribute [Fragment | Nil] The presenter notes as a Markly AST fragment.
|
|
129
175
|
attr :notes
|
|
130
176
|
|
|
177
|
+
# @attribute [String | Nil] JavaScript to execute after the slide renders on the display.
|
|
178
|
+
attr :script
|
|
179
|
+
|
|
131
180
|
# The template to use for rendering this slide.
|
|
132
181
|
# @returns [String] The template name from front_matter, or `"default"`.
|
|
133
182
|
def template
|
|
@@ -159,7 +208,7 @@ module Presently
|
|
|
159
208
|
end
|
|
160
209
|
|
|
161
210
|
# The transition type for animating into this slide.
|
|
162
|
-
# @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"
|
|
211
|
+
# @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"morph"`), or `nil` for instant swap.
|
|
163
212
|
def transition
|
|
164
213
|
@front_matter&.fetch("transition", nil)
|
|
165
214
|
end
|
|
@@ -36,6 +36,11 @@ module Presently
|
|
|
36
36
|
classes = [@css_class, extra_class].compact.join(" ")
|
|
37
37
|
builder.tag(:div, class: classes, data: {template: slide.template}) do
|
|
38
38
|
builder.raw(html)
|
|
39
|
+
if slide.script
|
|
40
|
+
builder.tag(:script, type: "text/slide-script") do
|
|
41
|
+
builder.raw(slide.script)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
end
|
|
@@ -63,14 +68,15 @@ module Presently
|
|
|
63
68
|
# @parameter name [String] The section name (derived from the Markdown heading).
|
|
64
69
|
# @returns [Boolean]
|
|
65
70
|
def section?(name)
|
|
66
|
-
|
|
71
|
+
fragment = @slide.content[name]
|
|
72
|
+
fragment && !fragment.empty?
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
# Get a named content section as raw HTML markup.
|
|
70
76
|
# @parameter name [String] The section name (derived from the Markdown heading).
|
|
71
77
|
# @returns [XRB::MarkupString] The rendered HTML content, safe for embedding.
|
|
72
78
|
def section(name)
|
|
73
|
-
XRB::MarkupString.raw(@slide.content[name] || "")
|
|
79
|
+
XRB::MarkupString.raw(@slide.content[name]&.to_html || "")
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
end
|
data/lib/presently/version.rb
CHANGED
data/public/_static/index.css
CHANGED
|
@@ -40,6 +40,7 @@ html, body {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
.slide-inner {
|
|
43
|
+
position: relative;
|
|
43
44
|
width: 100%;
|
|
44
45
|
height: 100%;
|
|
45
46
|
display: flex;
|
|
@@ -63,6 +64,14 @@ html, body {
|
|
|
63
64
|
|
|
64
65
|
.default-template .slide-body li {
|
|
65
66
|
margin-bottom: 0.5em;
|
|
67
|
+
transition: opacity 0.4s ease, transform 0.4s ease;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@starting-style {
|
|
71
|
+
.default-template .slide-body li {
|
|
72
|
+
opacity: 0;
|
|
73
|
+
transform: translateX(-0.5em);
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/* Title template */
|
|
@@ -125,6 +134,16 @@ html, body {
|
|
|
125
134
|
margin-bottom: 0.4em;
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
/* Diagram template */
|
|
138
|
+
.diagram-template {
|
|
139
|
+
padding: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.diagram-template .slide-body > div {
|
|
143
|
+
position: absolute;
|
|
144
|
+
box-sizing: border-box;
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
/* Image template */
|
|
129
148
|
.image-template .slide-caption {
|
|
130
149
|
font-size: 1.6rem;
|
|
@@ -353,12 +372,12 @@ html[data-transition="slide-right"]::view-transition-new(slide-container) {
|
|
|
353
372
|
/* Magic move — the browser interpolates position/size for matched
|
|
354
373
|
view-transition-name elements. No cross-fade on the container
|
|
355
374
|
to avoid background dimming. */
|
|
356
|
-
html[data-transition="
|
|
375
|
+
html[data-transition="morph"]::view-transition-old(slide-container) {
|
|
357
376
|
animation: none;
|
|
358
377
|
opacity: 0;
|
|
359
378
|
}
|
|
360
379
|
|
|
361
|
-
html[data-transition="
|
|
380
|
+
html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
362
381
|
animation: none;
|
|
363
382
|
}
|
|
364
383
|
|
|
@@ -392,6 +411,72 @@ html[data-transition="magic-move"]::view-transition-new(slide-container) {
|
|
|
392
411
|
to { transform: translateX(0); opacity: 1; }
|
|
393
412
|
}
|
|
394
413
|
|
|
414
|
+
/* ========================
|
|
415
|
+
BUILD EFFECTS
|
|
416
|
+
======================== */
|
|
417
|
+
|
|
418
|
+
/* Suppress both pseudo-elements for hidden build elements so they
|
|
419
|
+
neither crossfade in nor crossfade out during the transition. */
|
|
420
|
+
::view-transition-old(.build-hidden),
|
|
421
|
+
::view-transition-new(.build-hidden) {
|
|
422
|
+
display: none;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* Fade */
|
|
426
|
+
::view-transition-new(.build-fade) {
|
|
427
|
+
animation: vt-fade-in 0.4s ease;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* Fly in from left */
|
|
431
|
+
@keyframes build-fly-in-left {
|
|
432
|
+
from { transform: translateX(-2rem); opacity: 0; }
|
|
433
|
+
to { transform: translateX(0); opacity: 1; }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
::view-transition-new(.build-fly-left) {
|
|
437
|
+
animation: build-fly-in-left 0.4s ease;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* Fly in from right */
|
|
441
|
+
@keyframes build-fly-in-right {
|
|
442
|
+
from { transform: translateX(2rem); opacity: 0; }
|
|
443
|
+
to { transform: translateX(0); opacity: 1; }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
::view-transition-new(.build-fly-right) {
|
|
447
|
+
animation: build-fly-in-right 0.4s ease;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Fly in from bottom */
|
|
451
|
+
@keyframes build-fly-in-up {
|
|
452
|
+
from { transform: translateY(2rem); opacity: 0; }
|
|
453
|
+
to { transform: translateY(0); opacity: 1; }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
::view-transition-new(.build-fly-up) {
|
|
457
|
+
animation: build-fly-in-up 0.4s ease;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* Fly in from top */
|
|
461
|
+
@keyframes build-fly-in-down {
|
|
462
|
+
from { transform: translateY(-2rem); opacity: 0; }
|
|
463
|
+
to { transform: translateY(0); opacity: 1; }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
::view-transition-new(.build-fly-down) {
|
|
467
|
+
animation: build-fly-in-down 0.4s ease;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* Scale in */
|
|
471
|
+
@keyframes build-scale-in {
|
|
472
|
+
from { transform: scale(0.8); opacity: 0; }
|
|
473
|
+
to { transform: scale(1); opacity: 1; }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
::view-transition-new(.build-scale) {
|
|
477
|
+
animation: build-scale-in 0.4s ease;
|
|
478
|
+
}
|
|
479
|
+
|
|
395
480
|
/* ========================
|
|
396
481
|
PRESENTER VIEW
|
|
397
482
|
======================== */
|
|
@@ -617,6 +702,10 @@ html[data-transition="magic-move"]::view-transition-new(slide-container) {
|
|
|
617
702
|
margin: 0.3em 0;
|
|
618
703
|
}
|
|
619
704
|
|
|
705
|
+
.notes-content em {
|
|
706
|
+
color: var(--ahead);
|
|
707
|
+
}
|
|
708
|
+
|
|
620
709
|
.notes .no-notes {
|
|
621
710
|
opacity: 0.4;
|
|
622
711
|
font-style: italic;
|
data/public/application.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Live } from 'live';
|
|
2
2
|
import Syntax from '@socketry/syntax';
|
|
3
|
+
import { Slide } from './slide.js';
|
|
3
4
|
|
|
4
5
|
const live = Live.start();
|
|
5
6
|
|
|
@@ -65,6 +66,29 @@ function applyCodeFocus() {
|
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// Run the script for a single slide element.
|
|
70
|
+
// Wrapped in try/catch so syntax errors don't crash the presentation.
|
|
71
|
+
function runScript(slideEl) {
|
|
72
|
+
const scriptEl = slideEl.querySelector('script[type="text/slide-script"]');
|
|
73
|
+
if (!scriptEl) return;
|
|
74
|
+
|
|
75
|
+
const container = slideEl.querySelector('.slide-body') ?? slideEl;
|
|
76
|
+
const slide = new Slide(container);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const fn = new Function('slide', scriptEl.textContent);
|
|
80
|
+
fn(slide);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Slide script error:', error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Run scripts for all slide elements currently in the DOM.
|
|
87
|
+
function runSlideScripts() {
|
|
88
|
+
document.querySelectorAll('.slide').forEach(runScript);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
68
92
|
// Detect the transition type from the incoming HTML before morphdom applies it.
|
|
69
93
|
function detectTransition(html) {
|
|
70
94
|
const match = html.match(/data-transition="([^"]+)"/);
|
|
@@ -82,11 +106,12 @@ live.update = function(id, html, options) {
|
|
|
82
106
|
|
|
83
107
|
if (transition && document.startViewTransition && !activeTransition) {
|
|
84
108
|
document.documentElement.dataset.transition = transition;
|
|
85
|
-
|
|
109
|
+
|
|
86
110
|
activeTransition = document.startViewTransition(() => {
|
|
87
111
|
originalUpdate(id, html, options);
|
|
112
|
+
runSlideScripts();
|
|
88
113
|
});
|
|
89
|
-
|
|
114
|
+
|
|
90
115
|
activeTransition.finished.finally(() => {
|
|
91
116
|
delete document.documentElement.dataset.transition;
|
|
92
117
|
activeTransition = null;
|
|
@@ -95,6 +120,7 @@ live.update = function(id, html, options) {
|
|
|
95
120
|
});
|
|
96
121
|
} else {
|
|
97
122
|
originalUpdate(id, html, options);
|
|
123
|
+
runSlideScripts();
|
|
98
124
|
Syntax.highlight();
|
|
99
125
|
applyCodeFocus();
|
|
100
126
|
}
|
|
@@ -108,8 +134,9 @@ const observer = new MutationObserver(() => {
|
|
|
108
134
|
});
|
|
109
135
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
110
136
|
|
|
111
|
-
// Initial focus application:
|
|
137
|
+
// Initial focus and script application:
|
|
112
138
|
applyCodeFocus();
|
|
139
|
+
runSlideScripts();
|
|
113
140
|
|
|
114
141
|
// Jump-to select: forward the selected slide index to the presenter view.
|
|
115
142
|
document.addEventListener('change', (event) => {
|
data/public/slide.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Represents a collection of elements within a slide to be revealed sequentially.
|
|
2
|
+
// Has no side effects until build() is called.
|
|
3
|
+
export class SlideElements {
|
|
4
|
+
constructor(elements) {
|
|
5
|
+
this._elements = elements;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Show the first n elements and hide the rest.
|
|
9
|
+
// Assigns view-transition-names and applies build visibility in one step.
|
|
10
|
+
// @parameter n [Integer] Number of elements to show.
|
|
11
|
+
// @parameter options [Object]
|
|
12
|
+
// group: prefix for view-transition-name (default: "build")
|
|
13
|
+
// effect: "fade", "fly-up", "fly-down", "fly-left", "fly-right", "scale"
|
|
14
|
+
build(n, options = {}) {
|
|
15
|
+
const prefix = options.group || 'build';
|
|
16
|
+
|
|
17
|
+
this._elements.forEach((element, index) => {
|
|
18
|
+
element.style.viewTransitionName = `${prefix}-${index + 1}`;
|
|
19
|
+
|
|
20
|
+
if (index < n) {
|
|
21
|
+
element.style.visibility = 'visible';
|
|
22
|
+
// Newly revealed element: apply the enter effect.
|
|
23
|
+
// Already-visible elements: clear any class so they morph normally.
|
|
24
|
+
element.style.viewTransitionClass = (index === n - 1 && options.effect)
|
|
25
|
+
? `build-${options.effect}`
|
|
26
|
+
: '';
|
|
27
|
+
} else {
|
|
28
|
+
element.style.visibility = 'hidden';
|
|
29
|
+
// Hidden elements: suppress both pseudo-elements so they don't
|
|
30
|
+
// crossfade in or out during the transition.
|
|
31
|
+
element.style.viewTransitionClass = 'build-hidden';
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The scripting context passed to each slide's javascript block.
|
|
38
|
+
// Scopes element queries to the slide body.
|
|
39
|
+
export class Slide {
|
|
40
|
+
constructor(container) {
|
|
41
|
+
this._container = container;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find elements within this slide matching the given CSS selector.
|
|
45
|
+
// Use comma-separated selectors to combine multiple element types, e.g. "h2, li".
|
|
46
|
+
// @parameter selector [String] A CSS selector scoped to the slide body.
|
|
47
|
+
// @returns [SlideElements]
|
|
48
|
+
find(selector) {
|
|
49
|
+
const elements = Array.from(this._container.querySelectorAll(selector));
|
|
50
|
+
return new SlideElements(elements);
|
|
51
|
+
}
|
|
52
|
+
}
|
data/readme.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A web-based presentation tool built with [Lively](https://github.com/socketry/lively). Write your slides in Markdown, present them in the browser, and control everything from a separate presenter display.
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
[](https://github.com/socketry/presently/actions?workflow=Test)
|
|
6
8
|
|
|
7
9
|
## Features
|
|
@@ -17,13 +19,32 @@ A web-based presentation tool built with [Lively](https://github.com/socketry/li
|
|
|
17
19
|
|
|
18
20
|
## Usage
|
|
19
21
|
|
|
20
|
-
Please see the [project documentation](https://github.
|
|
22
|
+
Please see the [project documentation](https://socketry.github.io/presently/) for more details.
|
|
23
|
+
|
|
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.
|
|
21
25
|
|
|
22
|
-
- [
|
|
26
|
+
- [Animating Slides](https://socketry.github.io/presently/guides/animating-slides/index) - This guide explains how to animate content within slides using the `morph` transition and the slide scripting system.
|
|
23
27
|
|
|
24
28
|
## Releases
|
|
25
29
|
|
|
26
|
-
Please see the [project releases](https://github.
|
|
30
|
+
Please see the [project releases](https://socketry.github.io/presently/releases/index) for all releases.
|
|
31
|
+
|
|
32
|
+
### v0.4.0
|
|
33
|
+
|
|
34
|
+
- Add `bake presently:slides:notes` task to extract all presenter notes into a single Markdown document, with each slide's file path as a heading. Useful for reviewing or sharing speaker notes outside of the presentation.
|
|
35
|
+
- Presenter notes are now kept as a Markdown AST internally and rendered to HTML on demand, so the notes you write are faithfully round-tripped rather than converted to HTML at parse time.
|
|
36
|
+
|
|
37
|
+
### v0.3.0
|
|
38
|
+
|
|
39
|
+
- Add `diagram` template with a `position: relative` container — direct `<div>` children are `position: absolute` by default for free-form layouts.
|
|
40
|
+
- All slide templates now have `position: relative` on the slide inner container, allowing absolutely positioned overlays in any template.
|
|
41
|
+
- Add slide scripting: a fenced ` ```javascript ``` ` block at the end of presenter notes is extracted and executed in the browser after each slide renders. The script receives a `slide` object scoped to the slide body.
|
|
42
|
+
- Add `Slide#find(selector)` — a pure CSS selector query returning a `SlideElements` collection with no side effects.
|
|
43
|
+
- Add `SlideElements#build(n, options)` — shows the first `n` matched elements, hides the rest, and assigns `view-transition-name` for morph transition matching. Accepts `group` (name prefix) and `effect` (entry animation) options.
|
|
44
|
+
- Add build effects via `view-transition-class`: `fade`, `fly-left`, `fly-right`, `fly-up`, `fly-down`, `scale`. Requires Chromium 125+; degrades gracefully to instant appear in other browsers.
|
|
45
|
+
- Rename `magic-move` transition to `morph`.
|
|
46
|
+
- Italic text in presenter notes is styled in amber to distinguish stage directions from spoken words.
|
|
47
|
+
- Add transitions guide and animating slides guide to documentation.
|
|
27
48
|
|
|
28
49
|
### v0.2.0
|
|
29
50
|
|
data/releases.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.4.0
|
|
4
|
+
|
|
5
|
+
- Add `bake presently:slides:notes` task to extract all presenter notes into a single Markdown document, with each slide's file path as a heading. Useful for reviewing or sharing speaker notes outside of the presentation.
|
|
6
|
+
- Presenter notes are now kept as a Markdown AST internally and rendered to HTML on demand, so the notes you write are faithfully round-tripped rather than converted to HTML at parse time.
|
|
7
|
+
|
|
8
|
+
## v0.3.0
|
|
9
|
+
|
|
10
|
+
- Add `diagram` template with a `position: relative` container — direct `<div>` children are `position: absolute` by default for free-form layouts.
|
|
11
|
+
- All slide templates now have `position: relative` on the slide inner container, allowing absolutely positioned overlays in any template.
|
|
12
|
+
- Add slide scripting: a fenced ` ```javascript ``` ` block at the end of presenter notes is extracted and executed in the browser after each slide renders. The script receives a `slide` object scoped to the slide body.
|
|
13
|
+
- Add `Slide#find(selector)` — a pure CSS selector query returning a `SlideElements` collection with no side effects.
|
|
14
|
+
- Add `SlideElements#build(n, options)` — shows the first `n` matched elements, hides the rest, and assigns `view-transition-name` for morph transition matching. Accepts `group` (name prefix) and `effect` (entry animation) options.
|
|
15
|
+
- Add build effects via `view-transition-class`: `fade`, `fly-left`, `fly-right`, `fly-up`, `fly-down`, `scale`. Requires Chromium 125+; degrades gracefully to instant appear in other browsers.
|
|
16
|
+
- Rename `magic-move` transition to `morph`.
|
|
17
|
+
- Italic text in presenter notes is styled in amber to distinguish stage directions from spoken words.
|
|
18
|
+
- Add transitions guide and animating slides guide to documentation.
|
|
19
|
+
|
|
3
20
|
## v0.2.0
|
|
4
21
|
|
|
5
22
|
- Use Markly's native front matter parser (`Markly::FRONT_MATTER`) instead of manual string splitting, parsing each slide document once and extracting front matter directly from the AST.
|
data.tar.gz.sig
ADDED
|
Binary file
|
metadata
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: presently
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
8
8
|
bindir: bin
|
|
9
|
-
cert_chain:
|
|
9
|
+
cert_chain:
|
|
10
|
+
- |
|
|
11
|
+
-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
|
13
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
|
14
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
|
15
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
|
16
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
|
17
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
|
18
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
|
19
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
|
20
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
|
21
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
|
22
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
|
23
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
|
24
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
|
25
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
|
26
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
|
27
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
|
28
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
|
29
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
|
30
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
|
31
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
|
32
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
|
33
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
|
34
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
|
35
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
|
36
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
|
37
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
|
38
|
+
-----END CERTIFICATE-----
|
|
10
39
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
40
|
dependencies:
|
|
12
41
|
- !ruby/object:Gem::Dependency
|
|
@@ -1029,10 +1058,12 @@ files:
|
|
|
1029
1058
|
- public/_static/index.css
|
|
1030
1059
|
- public/application.js
|
|
1031
1060
|
- public/mermaid-diagram.js
|
|
1061
|
+
- public/slide.js
|
|
1032
1062
|
- readme.md
|
|
1033
1063
|
- releases.md
|
|
1034
1064
|
- templates/code.xrb
|
|
1035
1065
|
- templates/default.xrb
|
|
1066
|
+
- templates/diagram.xrb
|
|
1036
1067
|
- templates/fill.xrb
|
|
1037
1068
|
- templates/image.xrb
|
|
1038
1069
|
- templates/section.xrb
|
|
@@ -1043,6 +1074,7 @@ homepage: https://github.com/socketry/presently
|
|
|
1043
1074
|
licenses:
|
|
1044
1075
|
- MIT
|
|
1045
1076
|
metadata:
|
|
1077
|
+
documentation_uri: https://socketry.github.io/presently/
|
|
1046
1078
|
source_code_uri: https://github.com/socketry/presently.git
|
|
1047
1079
|
rdoc_options: []
|
|
1048
1080
|
require_paths:
|
metadata.gz.sig
ADDED
|
Binary file
|