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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b7e990c7b6d2f4ef66e333bc95793d0682011102614d92bf02c0278053a42c9
4
- data.tar.gz: 4f7ec48d87c12425dd4ecf02d7cf6c3530a9bdd1199ef030b0ab2fb8ed31e669
3
+ metadata.gz: 3991c95331fdcabd96b64dd78833690a52468f2a1e570eddf3234fd5e9bfa7ac
4
+ data.tar.gz: 816e1de43c33929e12424659455e116891294d79dcc3d2ea41cc8df9b13317dd
5
5
  SHA512:
6
- metadata.gz: 9d9629f50e5827eaf97d0fed33b01b1abfcece8f31a2232fc4c98ca765c3538e389fbb8a6f877a6ead1b9131e0a10ddabbfaf8d4b034c964012e86cad1c960c4
7
- data.tar.gz: 0f867425bd65d0d39d6252dabef8ffad6d9b952316b7305ed76397fae7561d07cea7656031ad153bd6fa42194e213beae69a65f2168b0599dbda3f74e3686a85
6
+ metadata.gz: bc5afae0aaeb288b51b4ccd6c8e66a562acd44898a49a3777299865ccff1a400258e88dd1fa3950071db8949b107fa076eac2d5042ccdd847e11d8d395801892
7
+ data.tar.gz: e0b1a9f6e5743beecdb4745469335c5ed8ad154e74bd5fdf2c745f54ce83532c1d81af3014c67ff8c9480e878b09a21e0df50cf73805e2a5676c0516a8ef807a
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,5 @@
1
+ (�e���3UQ��;��p9Vq+� r>���;���K�~��F,�4�ı1$cTq36�̾;���b�t>P�gB
2
+ �+��5����y�RV���_��
3
+ @Ԣ�l/��VbS��քz5`S4��7���HsI$~�P��(w
4
+ dq����88�lj�]5E���)�5��C#��Ч�gA�+]��l���nG�Uc8�s�*�ih5��:����8���e�%U�5���}>�f:��ntǓ� ��Y˶�F7��h6R�쎣�ߡ�{
5
+ /��b�
@@ -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(slide.notes)
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
@@ -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
- # Parses a Markdown slide file into structured data for {Slide}.
17
+ # A fragment of a Markly AST document.
18
18
  #
19
- # Handles YAML front_matter extraction, presenter note separation, and
20
- # Markdown-to-HTML rendering using the Markly AST.
21
- module Parser
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
- notes_fragment = Markly::Node.new(:document)
86
+ notes_node = Markly::Node.new(:document)
49
87
  while child = last_hrule.next
50
- notes_fragment.append_child(child)
88
+ notes_node.append_child(child)
51
89
  end
52
90
  last_hrule.delete
53
91
 
54
- content = parse_sections(document.each)
55
- notes = render_nodes(notes_fragment.each)
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.each)
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 list of AST nodes into sections based on top-level Markdown headings.
65
- # Each heading becomes a named key; content before the first heading
66
- # is collected under `"body"`. Headings inside code blocks are invisible
67
- # to this method as they never appear as top-level AST nodes.
68
- # @parameter nodes [Array(Markly::Node)] The nodes to parse into sections.
69
- # @returns [Hash(String, String)] Sections keyed by heading name, with rendered HTML values.
70
- def parse_sections(nodes)
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
- current_nodes = []
126
+ current_node = Markly::Node.new(:document)
74
127
 
75
- nodes.each do |node|
128
+ document.each do |node|
76
129
  if node.type == :header
77
- sections[current_key] = render_nodes(current_nodes) unless current_nodes.empty?
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
- current_nodes = []
132
+ current_node = Markly::Node.new(:document)
80
133
  else
81
- current_nodes << node
134
+ current_node.append_child(node.dup)
82
135
  end
83
136
  end
84
137
 
85
- sections[current_key] = render_nodes(current_nodes) unless current_nodes.empty?
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, String)] Content sections keyed by heading name.
111
- # @parameter notes [String | Nil] The rendered HTML presenter notes.
112
- def initialize(path, front_matter: nil, content: {}, notes: nil)
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, String)] The content sections keyed by heading name.
171
+ # @attribute [Hash(String, Fragment)] The content sections keyed by heading name.
126
172
  attr :content
127
173
 
128
- # @attribute [String | Nil] The rendered HTML presenter notes.
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"`, `"magic-move"`), or `nil` for instant swap.
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
- !(@slide.content[name] || "").empty?
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
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Presently
8
- VERSION = "0.2.0"
8
+ VERSION = "0.4.0"
9
9
  end
@@ -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="magic-move"]::view-transition-old(slide-container) {
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="magic-move"]::view-transition-new(slide-container) {
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;
@@ -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
+ ![Presenter Display](presenter.png)
6
+
5
7
  [![Development Status](https://github.com/socketry/presently/workflows/Test/badge.svg)](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.com/socketry/presently) for more details.
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
- - [Getting Started](https://github.com/socketry/presentlyguides/getting-started/index) - This guide explains how to use `presently` to create and deliver web-based presentations using Markdown slides.
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.com/socketry/presentlyreleases/index) for all releases.
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.
@@ -0,0 +1,5 @@
1
+ <div class="slide-inner diagram-template">
2
+ <div class="slide-body">
3
+ #{self.section("body")}
4
+ </div>
5
+ </div>
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.2.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