presently 0.3.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: cbb2d491928f3efc616d50e71f949a51d9ac4eca645a981372434a28855f86aa
4
- data.tar.gz: 1a8eed679adbe70ab3303912f67789fb17cae0a723e6a363a96609ff70ec8e39
3
+ metadata.gz: 3991c95331fdcabd96b64dd78833690a52468f2a1e570eddf3234fd5e9bfa7ac
4
+ data.tar.gz: 816e1de43c33929e12424659455e116891294d79dcc3d2ea41cc8df9b13317dd
5
5
  SHA512:
6
- metadata.gz: 40fba8bd5e994c33136e8627459a634b0c710539a19e06542ffcdacb525b003cff161418ab860c19a59ba91e8eabd2267a0b2bc004b2d509fc838ba8964ab8ab
7
- data.tar.gz: e312ec32e02e1d3f21a36eb064ccedc82280984289382ffe996e3c3a03c9973a84da795e6dfd85bbc2b34fd7e4c8834387480128440ffbd5ba8c2773a3625e51
6
+ metadata.gz: bc5afae0aaeb288b51b4ccd6c8e66a562acd44898a49a3777299865ccff1a400258e88dd1fa3950071db8949b107fa076eac2d5042ccdd847e11d8d395801892
7
+ data.tar.gz: e0b1a9f6e5743beecdb4745469335c5ed8ad154e74bd5fdf2c745f54ce83532c1d81af3014c67ff8c9480e878b09a21e0df50cf73805e2a5676c0516a8ef807a
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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,116 +14,145 @@ 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
+ # A fragment of a Markly AST document.
18
+ #
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
23
+ # Markly extensions enabled for all slide Markdown rendering.
24
+ EXTENSIONS = [:table, :tasklist, :strikethrough, :autolink]
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
+
17
58
  # Parses a Markdown slide file into structured data for {Slide}.
18
59
  #
19
60
  # Handles YAML front_matter extraction, presenter note separation, and
20
- # Markdown-to-HTML rendering using the Markly AST.
61
+ # Markdown AST construction via Markly.
21
62
  module Parser
22
- # Markly extensions enabled for all slide Markdown rendering.
23
- EXTENSIONS = [:table, :tasklist, :strikethrough, :autolink]
24
-
25
63
  module_function
26
-
64
+
27
65
  # Parse the file and return a {Slide}.
28
66
  # @parameter path [String] The file path to parse.
29
67
  # @returns [Slide]
30
68
  def load(path)
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)
35
-
72
+ document = Markly.parse(raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER, extensions: Fragment::EXTENSIONS)
73
+
36
74
  # Extract front matter from the first AST node if present.
37
75
  front_matter = nil
38
76
  if (front_matter_node = document.first_child) && front_matter_node.type == :front_matter
39
77
  front_matter = YAML.safe_load(front_matter_node.string_content)
40
78
  front_matter_node.delete
41
79
  end
42
-
80
+
43
81
  # Find the last hrule, which acts as the separator between slide content and presenter notes.
44
82
  last_hrule = nil
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
92
  # Extract the last javascript code block from the notes as the slide script.
55
93
  script_node = nil
56
- notes_fragment.each do |node|
94
+ notes_node.each do |node|
57
95
  if node.type == :code_block && node.fence_info.to_s.strip == "javascript"
58
96
  script_node = node
59
97
  end
60
98
  end
61
-
99
+
62
100
  script = nil
63
101
  if script_node
64
102
  script = script_node.string_content
65
103
  script_node.delete
66
104
  end
67
-
68
- content = parse_sections(document.each)
69
- notes = render_nodes(notes_fragment.each)
105
+
106
+ content = parse_sections(document)
107
+ notes = Fragment.new(notes_node)
70
108
  else
71
- content = parse_sections(document.each)
109
+ content = parse_sections(document)
72
110
  notes = nil
73
111
  script = nil
74
112
  end
75
-
113
+
76
114
  Slide.new(path, front_matter: front_matter, content: content, notes: notes, script: script)
77
115
  end
78
-
79
- # Parse a list of AST nodes into sections based on top-level Markdown headings.
80
- # Each heading becomes a named key; content before the first heading
81
- # is collected under `"body"`. Headings inside code blocks are invisible
82
- # to this method as they never appear as top-level AST nodes.
83
- # @parameter nodes [Array(Markly::Node)] The nodes to parse into sections.
84
- # @returns [Hash(String, String)] Sections keyed by heading name, with rendered HTML values.
85
- def parse_sections(nodes)
116
+
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)
86
124
  sections = {}
87
125
  current_key = "body"
88
- current_nodes = []
89
-
90
- nodes.each do |node|
126
+ current_node = Markly::Node.new(:document)
127
+
128
+ document.each do |node|
91
129
  if node.type == :header
92
- 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?
93
131
  current_key = node.to_plaintext.strip.downcase.gsub(/\s+/, "_")
94
- current_nodes = []
132
+ current_node = Markly::Node.new(:document)
95
133
  else
96
- current_nodes << node
134
+ current_node.append_child(node.dup)
97
135
  end
98
136
  end
99
-
100
- sections[current_key] = render_nodes(current_nodes) unless current_nodes.empty?
101
-
137
+
138
+ sections[current_key] = Fragment.new(current_node) unless current_node.first_child.nil?
139
+
102
140
  sections
103
141
  end
104
-
105
- # Render a list of AST nodes to HTML via a temporary document.
106
- # @parameter nodes [Array(Markly::Node)] The nodes to render.
107
- # @returns [String] The rendered HTML.
108
- def render_nodes(nodes)
109
- doc = Markly::Node.new(:document)
110
- nodes.each{|node| doc.append_child(node.dup)}
111
- Renderer.new(flags: Markly::UNSAFE, extensions: EXTENSIONS).render(doc)
112
- end
113
142
  end
114
-
143
+
115
144
  # Load and parse a slide from a Markdown file.
116
145
  # @parameter path [String] The file path to the Markdown slide.
117
146
  # @returns [Slide]
118
147
  def self.load(path)
119
148
  Parser.load(path)
120
149
  end
121
-
150
+
122
151
  # Initialize a slide with pre-parsed data.
123
152
  # @parameter path [String] The file path of the slide.
124
153
  # @parameter front_matter [Hash | Nil] The parsed YAML front_matter.
125
- # @parameter content [Hash(String, String)] Content sections keyed by heading name.
126
- # @parameter notes [String | Nil] The rendered HTML presenter notes.
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.
127
156
  # @parameter script [String | Nil] JavaScript to execute after the slide renders.
128
157
  def initialize(path, front_matter: nil, content: {}, notes: nil, script: nil)
129
158
  @path = path
@@ -132,58 +161,58 @@ module Presently
132
161
  @notes = notes
133
162
  @script = script
134
163
  end
135
-
164
+
136
165
  # @attribute [String] The file path of the slide.
137
166
  attr :path
138
-
167
+
139
168
  # @attribute [Hash | Nil] The parsed YAML front_matter.
140
169
  attr :front_matter
141
-
142
- # @attribute [Hash(String, String)] The content sections keyed by heading name.
170
+
171
+ # @attribute [Hash(String, Fragment)] The content sections keyed by heading name.
143
172
  attr :content
144
-
145
- # @attribute [String | Nil] The rendered HTML presenter notes.
173
+
174
+ # @attribute [Fragment | Nil] The presenter notes as a Markly AST fragment.
146
175
  attr :notes
147
-
176
+
148
177
  # @attribute [String | Nil] JavaScript to execute after the slide renders on the display.
149
178
  attr :script
150
-
179
+
151
180
  # The template to use for rendering this slide.
152
181
  # @returns [String] The template name from front_matter, or `"default"`.
153
182
  def template
154
183
  @front_matter&.fetch("template", "default") || "default"
155
184
  end
156
-
185
+
157
186
  # The expected duration of this slide in seconds.
158
187
  # @returns [Integer] The duration from front_matter, or `60`.
159
188
  def duration
160
189
  @front_matter&.fetch("duration", 60) || 60
161
190
  end
162
-
191
+
163
192
  # The title of this slide.
164
193
  # @returns [String] The title from front_matter, or the filename without extension.
165
194
  def title
166
195
  @front_matter&.fetch("title", File.basename(@path, ".md")) || File.basename(@path, ".md")
167
196
  end
168
-
197
+
169
198
  # Whether this slide should be skipped in the presentation.
170
199
  # @returns [Boolean]
171
200
  def skip?
172
201
  @front_matter&.fetch("skip", false) || false
173
202
  end
174
-
203
+
175
204
  # The navigation marker for this slide, used in the presenter's jump-to dropdown.
176
205
  # @returns [String | Nil] The marker label, or `nil` if not marked.
177
206
  def marker
178
207
  @front_matter&.fetch("marker", nil)
179
208
  end
180
-
209
+
181
210
  # The transition type for animating into this slide.
182
211
  # @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"morph"`), or `nil` for instant swap.
183
212
  def transition
184
213
  @front_matter&.fetch("transition", nil)
185
214
  end
186
-
215
+
187
216
  # The line range to focus on for code slides.
188
217
  # @returns [Array(Integer, Integer) | Nil] The `[start, end]` line numbers (1-based), or `nil`.
189
218
  def focus
@@ -68,14 +68,15 @@ module Presently
68
68
  # @parameter name [String] The section name (derived from the Markdown heading).
69
69
  # @returns [Boolean]
70
70
  def section?(name)
71
- !(@slide.content[name] || "").empty?
71
+ fragment = @slide.content[name]
72
+ fragment && !fragment.empty?
72
73
  end
73
74
 
74
75
  # Get a named content section as raw HTML markup.
75
76
  # @parameter name [String] The section name (derived from the Markdown heading).
76
77
  # @returns [XRB::MarkupString] The rendered HTML content, safe for embedding.
77
78
  def section(name)
78
- XRB::MarkupString.raw(@slide.content[name] || "")
79
+ XRB::MarkupString.raw(@slide.content[name]&.to_html || "")
79
80
  end
80
81
  end
81
82
  end
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Presently
8
- VERSION = "0.3.0"
8
+ VERSION = "0.4.0"
9
9
  end
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
@@ -19,14 +21,19 @@ A web-based presentation tool built with [Lively](https://github.com/socketry/li
19
21
 
20
22
  Please see the [project documentation](https://socketry.github.io/presently/) for more details.
21
23
 
22
- - [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
-
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 `morph` transition and the slide scripting system.
27
+
26
28
  ## Releases
27
29
 
28
30
  Please see the [project releases](https://socketry.github.io/presently/releases/index) for all releases.
29
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
+
30
37
  ### v0.3.0
31
38
 
32
39
  - Add `diagram` template with a `position: relative` container — direct `<div>` children are `position: absolute` by default for free-form layouts.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## v0.3.0
4
9
 
5
10
  - Add `diagram` template with a `position: relative` container — direct `<div>` children are `position: absolute` by default for free-form layouts.
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file