presently 0.20.0 → 0.21.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: 33cd4ebb1d6177f7409fbe3057946c3129d4e0017ba5fba671738bd3f1c2e85d
4
- data.tar.gz: 6d38e77b81eb7d9acce56a49b83478f276c938fc901b47cd0a4afe56b7c0fe6e
3
+ metadata.gz: 9d60b3fb59bbf07f0fdd4f3e1756998485049e2b55dc56225160e160af02346e
4
+ data.tar.gz: 4837aa9c8574f3b759a3199db53ad263d323df5cd6239df7292e8535ad83702c
5
5
  SHA512:
6
- metadata.gz: 76b50edaff01e665b361ed0f21730c2d049224a08971b965288253c8383bcfbd997edec1670daa4b4991ab073e9db4a08fc056a04c5cf1c48b57510f03aa9fed
7
- data.tar.gz: 472b3b21ba096b15675c98d4b53093ab49cf8fd508a5eceb90e5aa739c3de29b8ee39231a0bf4b88411df468780b6039004f2086c7ee96845ef300f7d04c0bd6
6
+ metadata.gz: b597972eabe2fec25758be28d3630db39756e93e7e2c24c43cfcdbaca3385741c1306748ada054cc7ea9d8bdbe9ba91f05217453cd5bfca3a1be5c2cc885b8d9
7
+ data.tar.gz: 122236fe265570f0be41163f662376b55df718d28ee52eada7e796c415ea52edd0f8f225649e1b9438d457cbb9281c2d06674274a80a33d6ec7b12ef304bed80
checksums.yaml.gz.sig CHANGED
Binary file
@@ -82,7 +82,7 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
82
82
  offset = prior_records.length
83
83
  slides.each_with_index do |slide, index|
84
84
  relative_path = slide.path
85
- headline = extract_headline(slide)
85
+ headline = slide.title
86
86
  absolute = offset + index + 1
87
87
  total = offset + slides.length
88
88
 
@@ -199,14 +199,6 @@ private
199
199
 
200
200
  UPDATE_THRESHOLD = 1.0
201
201
 
202
- def extract_headline(slide)
203
- body = slide.content["body"]
204
- return slide.title unless body && !body.empty?
205
-
206
- first_line = body.to_commonmark.strip.lines.first.to_s.strip
207
- first_line.empty? ? slide.title : first_line
208
- end
209
-
210
202
  def print_report(records, log_path)
211
203
  planned_total = records.sum{|record| record[:planned]}
212
204
  actual_total = records.sum{|record| record[:actual]}
@@ -60,11 +60,7 @@ template: title
60
60
  duration: 30
61
61
  ---
62
62
 
63
- # Title
64
-
65
- Welcome to My Talk
66
-
67
- # Subtitle
63
+ # Welcome to My Talk
68
64
 
69
65
  A presentation built with Presently
70
66
 
@@ -76,7 +72,7 @@ These are presenter notes — only visible in the presenter view.
76
72
  Each slide has three parts:
77
73
 
78
74
  1. **YAML frontmatter** between `---` markers at the top, specifying the template, duration, and other metadata.
79
- 2. **Content** with Markdown headings that become named sections for the template.
75
+ 2. **Content** with an optional H1 title and any template placeholders written as H2 sections.
80
76
  3. **Presenter notes** after a `---` separator in the body (optional).
81
77
 
82
78
  ### Styling Slides
@@ -196,7 +192,7 @@ Templates define the visual layout of each slide. Select a template using the `t
196
192
 
197
193
  ### Default
198
194
 
199
- A general-purpose content slide. All content without a heading goes into the `body` section.
195
+ A general-purpose content slide. An H1 becomes the slide title and the remaining document becomes its body.
200
196
 
201
197
  ``` markdown
202
198
  ---
@@ -204,6 +200,8 @@ template: default
204
200
  duration: 60
205
201
  ---
206
202
 
203
+ # Key points
204
+
207
205
  - First point
208
206
  - Second point
209
207
  - Third point
@@ -211,7 +209,7 @@ duration: 60
211
209
 
212
210
  ### Title
213
211
 
214
- A large title with a subtitle, centered on the slide.
212
+ A large title with a short body, centered on the slide.
215
213
 
216
214
  ``` markdown
217
215
  ---
@@ -219,18 +217,14 @@ template: title
219
217
  duration: 30
220
218
  ---
221
219
 
222
- # Title
223
-
224
- My Presentation Title
225
-
226
- # Subtitle
220
+ # My Presentation Title
227
221
 
228
222
  A subtitle or tagline
229
223
  ```
230
224
 
231
225
  ### Section
232
226
 
233
- A section divider slide with a large heading and accent background.
227
+ A section divider slide with a large heading, optional supporting body, and accent background.
234
228
 
235
229
  ``` markdown
236
230
  ---
@@ -238,9 +232,9 @@ template: section
238
232
  duration: 15
239
233
  ---
240
234
 
241
- # Heading
235
+ # Part Two
242
236
 
243
- Part Two
237
+ Architecture and design
244
238
  ```
245
239
 
246
240
  ### Two Column
@@ -253,14 +247,18 @@ template: two_column
253
247
  duration: 90
254
248
  ---
255
249
 
256
- # Left
250
+ # Client and server responsibilities
251
+
252
+ The application is split across two cooperating environments.
253
+
254
+ ## Left
257
255
 
258
256
  **Server Side**
259
257
 
260
258
  - Ruby + Lively
261
259
  - WebSocket connections
262
260
 
263
- # Right
261
+ ## Right
264
262
 
265
263
  **Client Side**
266
264
 
@@ -277,9 +275,10 @@ A syntax-highlighted code slide with optional focus regions for code walkthrough
277
275
  template: code
278
276
  duration: 60
279
277
  focus: 2-8
280
- title: Constructor
281
278
  ---
282
279
 
280
+ # Constructor
281
+
283
282
  ```ruby
284
283
  class Presentation
285
284
  def initialize
@@ -298,7 +297,7 @@ Create animated walkthroughs by using multiple slides with the same code but dif
298
297
 
299
298
  ### Statement
300
299
 
301
- A prominent statement or quote, centered on the slide. Supports an optional `# Translation` section.
300
+ A prominent statement or quote, centered on the slide. Supports an optional `## Translation` placeholder.
302
301
 
303
302
  ``` markdown
304
303
  ---
@@ -308,14 +307,14 @@ duration: 30
308
307
 
309
308
  The best way to predict the future is to create it.
310
309
 
311
- # Translation
310
+ ## Translation
312
311
 
313
312
  未来を予測する最善の方法は、それを創ることである。
314
313
  ```
315
314
 
316
315
  ### Translations
317
316
 
318
- All templates support an optional `# Translation` section. When present, the translation is displayed below the main content in a lighter style. This works with `title`, `section`, `statement`, and `image` templates.
317
+ Templates can extract an optional `## Translation` section and position it independently from the main document. Every standard template displays it separately in a lighter style.
319
318
 
320
319
  ### Image
321
320
 
@@ -329,7 +328,7 @@ duration: 30
329
328
 
330
329
  ![Architecture diagram](/images/architecture.png)
331
330
 
332
- # Caption
331
+ ## Caption
333
332
 
334
333
  System architecture overview
335
334
  ```
@@ -344,11 +343,7 @@ template: diagram
344
343
  duration: 60
345
344
  ---
346
345
 
347
- # Title
348
-
349
- Request lifecycle
350
-
351
- # Body
346
+ # Request lifecycle
352
347
 
353
348
  <div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 2em; width: 80%;">
354
349
  <div>Browser</div>
@@ -420,7 +415,20 @@ service "presently" do
420
415
  end
421
416
  ```
422
417
 
423
- Templates receive a {ruby Presently::TemplateScope} with access to `self.slide` (the {ruby Presently::Slide} instance) and `self.section(name)` for retrieving named content sections.
418
+ Templates receive a {ruby Presently::TemplateScope}. `self.slide_header` renders the semantic H1 title and optional section metadata, while `self.document` renders the remaining slide body. `self.extract(name)` removes an H2 placeholder with that exact heading text from the body and returns its rendered content. Extract placeholders before rendering the remaining document:
419
+
420
+ ``` xrb
421
+ <?r translation = self.extract("Translation") ?>
422
+ #{self.slide_header}
423
+ <div class="slide-body">
424
+ #{self.document}
425
+ </div>
426
+ <?r if translation ?>
427
+ <div class="slide-translation">#{translation}</div>
428
+ <?r end ?>
429
+ ```
430
+
431
+ Extraction stops at the next heading of the same or a higher level, so lower-level headings remain inside the extracted fragment. Placeholder names are case-sensitive and must match the heading text exactly. Only placeholders requested by the template are removed; other headings remain ordinary document content.
424
432
 
425
433
  ## Customizing the Application
426
434
 
@@ -14,13 +14,13 @@ require_relative "stylesheet"
14
14
  module Presently
15
15
  # A single slide parsed from a Markdown file.
16
16
  #
17
- # Each slide has YAML front_matter for metadata (template, duration, focus), content sections
18
- # split by Markdown headings, and optional presenter notes separated by `---`.
17
+ # Each slide has YAML front_matter for metadata (template, duration, focus), a Markdown
18
+ # document, and optional presenter notes separated by `---`.
19
19
  class Slide
20
20
  # A fragment of a Markly AST document.
21
21
  #
22
22
  # Wraps a `Markly::Node` of type `:document` and provides rendering helpers.
23
- # Used for both content sections and presenter notes so callers can choose
23
+ # Used for both slide documents and presenter notes so callers can choose
24
24
  # their output format without the parser pre-committing to one.
25
25
  class Fragment
26
26
  # Markly extensions enabled for all slide Markdown rendering.
@@ -35,6 +35,13 @@ module Presently
35
35
  # @attribute [Markly::Node] The underlying AST document node.
36
36
  attr :node
37
37
 
38
+ # Duplicate this fragment and its underlying AST.
39
+ # @returns [Fragment] An independently mutable copy.
40
+ def initialize_copy(other)
41
+ super
42
+ @node = other.node.dup
43
+ end
44
+
38
45
  # Whether the fragment has no content.
39
46
  # @returns [Boolean]
40
47
  def empty?
@@ -47,6 +54,51 @@ module Presently
47
54
  Markly::Renderer::HTML.new(flags: Markly::UNSAFE, extensions: EXTENSIONS).render(@node)
48
55
  end
49
56
 
57
+ # Extract the content belonging to a named H2 placeholder.
58
+ #
59
+ # The matching heading itself is removed. Following nodes are extracted up
60
+ # to the next heading of the same or a higher level. Lower-level headings
61
+ # remain part of the extracted content.
62
+ # @parameter name [String] The heading name to extract.
63
+ # @returns [Fragment | Nil] The extracted content, or `nil` when not found.
64
+ def extract(name)
65
+ heading = find_heading(name)
66
+ return unless heading
67
+
68
+ level = heading.header_level
69
+ fragment = Markly::Node.new(:document)
70
+ node = heading.next
71
+ heading.delete
72
+
73
+ while node
74
+ next_node = node.next
75
+ break if node.type == :header && node.header_level <= level
76
+
77
+ fragment.append_child(node)
78
+ node = next_node
79
+ end
80
+
81
+ Fragment.new(fragment)
82
+ end
83
+
84
+ # Extract the first heading at the given level, retaining the heading node.
85
+ # @parameter level [Integer] The Markdown heading level.
86
+ # @returns [Fragment | Nil] A fragment containing the extracted heading.
87
+ def extract_heading(level = 1)
88
+ heading = nil
89
+ @node.each do |node|
90
+ if node.type == :header && node.header_level == level
91
+ heading = node
92
+ break
93
+ end
94
+ end
95
+ return unless heading
96
+
97
+ fragment = Markly::Node.new(:document)
98
+ fragment.append_child(heading)
99
+ Fragment.new(fragment)
100
+ end
101
+
50
102
  # Render the fragment back to CommonMark Markdown.
51
103
  # @returns [String] The CommonMark source.
52
104
  def to_commonmark
@@ -54,6 +106,27 @@ module Presently
54
106
  end
55
107
 
56
108
  alias to_s to_commonmark
109
+
110
+ # Return the plain text of the first heading at the given level.
111
+ # @parameter level [Integer] The Markdown heading level.
112
+ # @returns [String | Nil] The heading text, or `nil` when not found.
113
+ def heading_text(level = 1)
114
+ @node.each do |node|
115
+ if node.type == :header && node.header_level == level
116
+ return node.dup.extract_children.to_plaintext
117
+ end
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def find_heading(name)
124
+ @node.each do |node|
125
+ return node if node.type == :header && node.header_level == 2 && node.dup.extract_children.to_plaintext == name
126
+ end
127
+
128
+ nil
129
+ end
57
130
  end
58
131
 
59
132
  # Parses a Markdown slide file into structured data for {Slide}.
@@ -61,8 +134,17 @@ module Presently
61
134
  # Handles YAML front_matter extraction, presenter note separation, and
62
135
  # Markdown AST construction via Markly.
63
136
  module Parser
137
+ FLAGS = Markly::UNSAFE | Markly::FRONT_MATTER | Markly::INLINE_CODE_INFO | Markly::HTML_BLOCK_BLANK_LINES
138
+
64
139
  module_function
65
140
 
141
+ # Parse slide Markdown with the standard flags and extensions.
142
+ # @parameter source [String] The Markdown source.
143
+ # @returns [Markly::Node] The parsed document.
144
+ def parse(source)
145
+ Markly.parse(source, flags: FLAGS, extensions: Fragment::EXTENSIONS)
146
+ end
147
+
66
148
  # Parse the file and return a {Slide}.
67
149
  # @parameter presentation [Presentation] The presentation which owns the slide.
68
150
  # @parameter path [String] The slide path relative to the presentation root.
@@ -72,7 +154,7 @@ module Presently
72
154
  raw = File.read(source_path)
73
155
 
74
156
  # Parse once, with native front matter support.
75
- document = Markly.parse(raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER | Markly::INLINE_CODE_INFO | Markly::HTML_BLOCK_BLANK_LINES, extensions: Fragment::EXTENSIONS)
157
+ document = parse(raw)
76
158
 
77
159
  expand_includes!(document, File.dirname(source_path), presentation.root)
78
160
  rewrite_image_urls!(document, source_path, presentation.root)
@@ -109,14 +191,12 @@ module Presently
109
191
  script_node.delete
110
192
  end
111
193
 
112
- content = parse_sections(document)
113
194
  notes = Fragment.new(notes_node)
114
195
  else
115
- content = parse_sections(document)
116
196
  notes = nil
117
197
  end
118
198
 
119
- Slide.new(presentation, path, front_matter: front_matter, content: content, notes: notes, scripts: scripts)
199
+ Slide.new(presentation, path, front_matter: front_matter, document: Fragment.new(document), notes: notes, scripts: scripts)
120
200
  end
121
201
 
122
202
  # Extract reusable setup scripts from the expanded slide document.
@@ -169,7 +249,7 @@ module Presently
169
249
  to_replace.each do |paragraph, relative_path|
170
250
  included_path = File.expand_path(relative_path, base_dir)
171
251
  included_raw = File.read(included_path)
172
- included_document = Markly.parse(included_raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER | Markly::INLINE_CODE_INFO | Markly::HTML_BLOCK_BLANK_LINES, extensions: Fragment::EXTENSIONS)
252
+ included_document = parse(included_raw)
173
253
 
174
254
  # Strip front matter from included file if present.
175
255
  front_matter_node = included_document.first_child
@@ -211,31 +291,6 @@ module Presently
211
291
  end
212
292
  end
213
293
 
214
- # Parse a Markly document into content sections based on top-level headings.
215
- #
216
- # Each heading becomes a named key; content before the first heading is
217
- # collected under `"body"`. Each value is a {Fragment} wrapping a document node.
218
- # @parameter document [Markly::Node] The document to parse.
219
- # @returns [Hash(String, Fragment)] Sections keyed by heading name.
220
- def parse_sections(document)
221
- sections = {}
222
- current_key = "body"
223
- current_node = Markly::Node.new(:document)
224
-
225
- document.each do |node|
226
- if node.type == :header
227
- sections[current_key] = Fragment.new(current_node) unless current_node.first_child.nil?
228
- current_key = node.to_plaintext.strip.downcase.gsub(/\s+/, "_")
229
- current_node = Markly::Node.new(:document)
230
- else
231
- current_node.append_child(node.dup)
232
- end
233
- end
234
-
235
- sections[current_key] = Fragment.new(current_node) unless current_node.first_child.nil?
236
-
237
- sections
238
- end
239
294
  end
240
295
 
241
296
  # Load and parse a slide from a Markdown file.
@@ -250,18 +305,16 @@ module Presently
250
305
  # @parameter presentation [Presentation] The presentation which owns the slide.
251
306
  # @parameter path [String] The slide path relative to the presentation root.
252
307
  # @parameter front_matter [Hash | Nil] The parsed YAML front_matter.
253
- # @parameter content [Hash(String, Fragment)] Content sections keyed by heading name.
308
+ # @parameter document [Fragment | Nil] The complete slide document.
254
309
  # @parameter notes [Fragment | Nil] The presenter notes as a Markly AST fragment.
255
310
  # @parameter scripts [Array(String)] JavaScript sources to execute after the slide renders.
256
- # @parameter script [String | Nil] A single JavaScript source retained for compatibility.
257
- def initialize(presentation, path, front_matter: nil, content: {}, notes: nil, scripts: [], script: nil)
311
+ def initialize(presentation, path, front_matter: nil, document: nil, notes: nil, scripts: [])
258
312
  @presentation = presentation
259
313
  @path = path
260
314
  @front_matter = front_matter
261
- @content = content
315
+ @document = document || Fragment.new(Markly::Node.new(:document))
262
316
  @notes = notes
263
317
  @scripts = scripts.dup
264
- @scripts << script if script
265
318
  end
266
319
 
267
320
  # @attribute [Presentation] The presentation which owns the slide.
@@ -279,8 +332,8 @@ module Presently
279
332
  # @attribute [Hash | Nil] The parsed YAML front_matter.
280
333
  attr :front_matter
281
334
 
282
- # @attribute [Hash(String, Fragment)] The content sections keyed by heading name.
283
- attr :content
335
+ # @attribute [Fragment] The complete slide document.
336
+ attr :document
284
337
 
285
338
  # @attribute [Fragment | Nil] The presenter notes as a Markly AST fragment.
286
339
  attr :notes
@@ -288,14 +341,6 @@ module Presently
288
341
  # @attribute [Array(String)] JavaScript sources to execute after the slide renders on the display.
289
342
  attr :scripts
290
343
 
291
- # All JavaScript sources combined into a single string.
292
- # @returns [String | Nil] The combined JavaScript, or `nil` when the slide has no scripts.
293
- def script
294
- return if @scripts.empty?
295
-
296
- @scripts.join("\n\n")
297
- end
298
-
299
344
  # The template to use for rendering this slide.
300
345
  # @returns [String] The template name from front_matter, or `"default"`.
301
346
  def template
@@ -309,9 +354,15 @@ module Presently
309
354
  end
310
355
 
311
356
  # The title of this slide.
312
- # @returns [String] The title from front_matter, or the filename without extension.
357
+ # @returns [String] The H1 text, front matter title, or filename without extension.
313
358
  def title
314
- @front_matter&.fetch("title", File.basename(@path, ".md")) || File.basename(@path, ".md")
359
+ @document.heading_text(1) || @front_matter&.fetch("title", nil) || File.basename(@path, ".md")
360
+ end
361
+
362
+ # The section name for this slide.
363
+ # @returns [String | Nil] The section from front matter, or `nil` when it was not specified.
364
+ def section
365
+ @front_matter&.fetch("section", nil)
315
366
  end
316
367
 
317
368
  # Whether this slide should be skipped in the presentation.
@@ -64,36 +64,75 @@ module Presently
64
64
 
65
65
  # Provides the scope for XRB template rendering.
66
66
  #
67
- # Templates access slide content via `self.section(name)` and slide metadata via `self.slide`.
67
+ # Templates selectively extract placeholders with `self.extract(name)` and render the remainder
68
+ # with `self.document`.
68
69
  class TemplateScope
69
70
  # Initialize a new template scope for the given slide.
70
71
  # @parameter slide [Slide] The slide being rendered.
71
72
  def initialize(slide)
72
73
  @slide = slide
74
+ @document = slide.document.dup
75
+ @heading = @document.extract_heading(1)
76
+ @extracted = {}
73
77
  end
74
78
 
75
79
  # @attribute [Slide] The slide being rendered.
76
80
  attr :slide
77
81
 
78
- # The content sections of the slide.
79
- # @returns [Hash(String, String)] Sections keyed by heading name.
80
- def content
81
- @slide.content
82
+ # Render the slide body after title and placeholder extraction.
83
+ # @returns [XRB::MarkupString] The remaining document as HTML.
84
+ def document
85
+ markup(@document)
82
86
  end
83
87
 
84
- # Whether the named content section exists and has content.
85
- # @parameter name [String] The section name (derived from the Markdown heading).
86
- # @returns [Boolean]
87
- def section?(name)
88
- fragment = @slide.content[name]
89
- fragment && !fragment.empty?
88
+ # Extract and render a named H2 placeholder from this render's document.
89
+ #
90
+ # Extraction is cached so a template can reference a placeholder more than
91
+ # once without mutating the document repeatedly.
92
+ # @parameter name [String] The heading name to extract.
93
+ # @returns [XRB::MarkupString | Nil] The extracted HTML, or `nil` when absent.
94
+ def extract(name)
95
+ fragment = extract_fragment(name)
96
+ return unless fragment && !fragment.empty?
97
+
98
+ markup(fragment)
99
+ end
100
+
101
+ # Render the slide header using semantic markup.
102
+ # @returns [XRB::MarkupString] The rendered header, or an empty string when no metadata is present.
103
+ def slide_header
104
+ section = @slide.section
105
+
106
+ return XRB::MarkupString.raw("") unless present?(section) || @heading
107
+
108
+ builder = XRB::Builder.new
109
+ builder.tag(:header, class: "slide-header") do
110
+ if present?(section)
111
+ builder.tag(:div, class: "slide-section-heading") do
112
+ builder.text(section.to_s)
113
+ end
114
+ end
115
+
116
+ builder.raw(@heading.to_html) if @heading
117
+ end
118
+
119
+ XRB::MarkupString.raw(builder.to_s)
120
+ end
121
+
122
+ private
123
+
124
+ def extract_fragment(name)
125
+ return @extracted[name] if @extracted.key?(name)
126
+
127
+ @extracted[name] = @document.extract(name)
128
+ end
129
+
130
+ def markup(fragment)
131
+ XRB::MarkupString.raw(fragment&.to_html || "")
90
132
  end
91
133
 
92
- # Get a named content section as raw HTML markup.
93
- # @parameter name [String] The section name (derived from the Markdown heading).
94
- # @returns [XRB::MarkupString] The rendered HTML content, safe for embedding.
95
- def section(name)
96
- XRB::MarkupString.raw(@slide.content[name]&.to_html || "")
134
+ def present?(value)
135
+ value && !value.to_s.empty?
97
136
  end
98
137
  end
99
138
  end
@@ -24,7 +24,7 @@ module Presently
24
24
  end
25
25
 
26
26
  # Initialize with a fully-resolved root list.
27
- # Prefer {.build} for normal use; use this when you already have the complete list.
27
+ # Prefer {.for} for normal use; use this when you already have the complete list.
28
28
  # @parameter roots [Array(String)] The complete ordered list of directories to search.
29
29
  def initialize(roots = [DEFAULT_ROOT])
30
30
  @roots = roots
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Presently
8
- VERSION = "0.20.0"
8
+ VERSION = "0.21.0"
9
9
  end
@@ -51,7 +51,6 @@ html, body {
51
51
 
52
52
  .slide {
53
53
  --slide-font-size: 2.2cqi;
54
- --slide-font-body: 1em;
55
54
  --slide-font-title: 1.8em;
56
55
  --slide-font-heading: 1.6em;
57
56
  --slide-font-statement: 1.5em;
@@ -89,10 +88,33 @@ html, body {
89
88
  line-height: 1.5;
90
89
  }
91
90
 
91
+ .slide-header {
92
+ flex: none;
93
+ width: 100%;
94
+ padding: 0;
95
+ background: none;
96
+ border: 0;
97
+ margin-bottom: var(--slide-gap);
98
+ }
99
+
100
+ .slide-section-heading {
101
+ margin-bottom: calc(var(--slide-gap) * 0.25);
102
+ color: var(--accent-light);
103
+ font-family: var(--font-mono);
104
+ font-size: 0.45em;
105
+ font-weight: 700;
106
+ letter-spacing: 0.08em;
107
+ text-transform: uppercase;
108
+ }
109
+
110
+ .slide-header > h1 {
111
+ margin: 0;
112
+ font-size: var(--slide-font-heading);
113
+ line-height: 1.1;
114
+ }
115
+
92
116
  /* Default template */
93
117
  .default-template .slide-body {
94
- font-size: var(--slide-font-body);
95
- line-height: 1.5;
96
118
  width: 100%;
97
119
  }
98
120
 
@@ -118,22 +140,19 @@ html, body {
118
140
  text-align: center;
119
141
  }
120
142
 
121
- .title-template .slide-title {
122
- font-size: var(--slide-font-title);
123
- font-weight: 700;
143
+ .title-template .slide-header {
124
144
  margin-bottom: calc(var(--slide-gap) * 0.75);
125
145
  }
126
146
 
127
- .title-template .slide-title p {
128
- margin: 0;
147
+ .title-template .slide-header > h1 {
148
+ font-size: var(--slide-font-title);
129
149
  }
130
150
 
131
- .title-template .slide-subtitle {
132
- font-size: var(--slide-font-body);
151
+ .title-template .slide-body {
133
152
  opacity: 0.7;
134
153
  }
135
154
 
136
- .title-template .slide-subtitle p {
155
+ .title-template .slide-body p {
137
156
  margin: 0;
138
157
  }
139
158
 
@@ -146,22 +165,49 @@ html, body {
146
165
  text-align: center;
147
166
  }
148
167
 
149
- .section-template .slide-heading {
150
- font-size: var(--slide-font-heading);
168
+ .section-template .slide-header {
169
+ margin-bottom: 0;
170
+ }
171
+
172
+ .section-template .slide-header > h1 {
151
173
  font-weight: 700;
152
174
  }
153
175
 
154
- .section-template .slide-heading p {
176
+ .section-template .slide-body {
177
+ margin-top: calc(var(--slide-gap) * 0.75);
178
+ opacity: 0.7;
179
+ }
180
+
181
+ .section-template .slide-body p {
155
182
  margin: 0;
156
183
  }
157
184
 
158
185
  /* Two column template */
159
186
  .two-column-template {
160
- flex-direction: row;
161
- gap: calc(var(--slide-gap) * 1.5);
162
187
  padding: calc(var(--slide-gutter) * 0.75) var(--slide-gutter);
163
188
  }
164
189
 
190
+ .two-column-template .slide-columns {
191
+ display: flex;
192
+ align-items: center;
193
+ flex: 1;
194
+ width: 100%;
195
+ gap: calc(var(--slide-gap) * 1.5);
196
+ }
197
+
198
+ .two-column-template > .slide-body {
199
+ width: 100%;
200
+ margin-bottom: var(--slide-gap);
201
+ }
202
+
203
+ .two-column-template > .slide-body > :first-child {
204
+ margin-top: 0;
205
+ }
206
+
207
+ .two-column-template > .slide-body > :last-child {
208
+ margin-bottom: 0;
209
+ }
210
+
165
211
  .two-column-template .column {
166
212
  flex: 1;
167
213
  min-width: 0;
@@ -178,23 +224,10 @@ html, body {
178
224
  }
179
225
 
180
226
  /* Diagram template */
181
- .diagram-template {
182
- padding: var(--slide-gutter);
183
- }
184
-
185
- .diagram-template .slide-title {
186
- flex: none;
187
- width: 100%;
188
- margin-bottom: var(--slide-gap);
189
- font-size: var(--slide-font-body);
190
- font-weight: 700;
227
+ .diagram-template .slide-header {
191
228
  text-align: center;
192
229
  }
193
230
 
194
- .diagram-template .slide-title p {
195
- margin: 0;
196
- }
197
-
198
231
  .diagram-template .slide-body {
199
232
  display: grid;
200
233
  flex: 1;
@@ -356,6 +389,10 @@ pre > syntax-code {
356
389
  width: 100%;
357
390
  }
358
391
 
392
+ .statement-template .slide-header {
393
+ margin-bottom: 0;
394
+ }
395
+
359
396
  .statement-template .slide-statement mermaid-diagram {
360
397
  font-weight: normal;
361
398
  }
@@ -386,11 +423,17 @@ mermaid-diagram {
386
423
  /* Fullscreen template */
387
424
  .fill-template {
388
425
  padding: 0;
389
- width: 100%;
390
- height: 100%;
391
426
  }
392
427
 
393
- .fill-template .slide-content {
428
+ .fill-template .slide-header {
429
+ position: absolute;
430
+ top: var(--slide-gutter);
431
+ left: var(--slide-gutter);
432
+ z-index: 1;
433
+ width: calc(100% - var(--slide-gutter) * 2);
434
+ }
435
+
436
+ .fill-template .slide-body {
394
437
  width: 100%;
395
438
  height: 100%;
396
439
  display: flex;
@@ -398,10 +441,19 @@ mermaid-diagram {
398
441
  justify-content: center;
399
442
  }
400
443
 
401
- .fill-template .slide-content img,
402
- .fill-template .slide-content iframe,
403
- .fill-template .slide-content svg,
404
- .fill-template .slide-content video {
444
+ .fill-template .slide-translation {
445
+ position: absolute;
446
+ bottom: var(--slide-gutter);
447
+ left: var(--slide-gutter);
448
+ z-index: 1;
449
+ width: calc(100% - var(--slide-gutter) * 2);
450
+ text-align: center;
451
+ }
452
+
453
+ .fill-template .slide-body img,
454
+ .fill-template .slide-body iframe,
455
+ .fill-template .slide-body svg,
456
+ .fill-template .slide-body video {
405
457
  width: 100%;
406
458
  height: 100%;
407
459
  object-fit: contain;
@@ -414,14 +466,12 @@ mermaid-diagram {
414
466
  align-items: stretch;
415
467
  }
416
468
 
417
- .code-template .code-title {
418
- font-size: var(--slide-font-secondary);
419
- font-weight: 600;
469
+ .code-template .slide-header {
420
470
  margin-bottom: calc(var(--slide-gap) * 0.5);
421
471
  }
422
472
 
423
- .code-template .code-title p {
424
- margin: 0;
473
+ .code-template .slide-header > h1 {
474
+ font-size: var(--slide-font-secondary);
425
475
  }
426
476
 
427
477
  .code-viewport {
@@ -760,6 +810,7 @@ body.playback[data-controls="false"] .playback-start-screen {
760
810
  display: flex;
761
811
  align-items: center;
762
812
  gap: 1rem;
813
+ min-width: 0;
763
814
  }
764
815
 
765
816
  .toolbar > button,
data/readme.md CHANGED
@@ -67,6 +67,15 @@ The task records the presentation at 1920×1080 and 30 frames per second by defa
67
67
 
68
68
  Please see the [project releases](https://socketry.github.io/presently/releases/index) for all releases.
69
69
 
70
+ ### v0.21.0
71
+
72
+ - [Semantic Slides](https://socketry.github.io/presently/releases/index#semantic-slides)
73
+ - [Slide Scripts](https://socketry.github.io/presently/releases/index#slide-scripts)
74
+
75
+ ### v0.20.1
76
+
77
+ - Keep presenter toolbars within the viewport when slide paths are long.
78
+
70
79
  ### v0.19.0
71
80
 
72
81
  - Keep Anime.js resources created by delayed slide callbacks bound to the slide lifecycle.
@@ -112,14 +121,6 @@ q
112
121
 
113
122
  - Change zoom to 50% on slide preview (presenter display).
114
123
 
115
- ### v0.12.0
116
-
117
- - Add support for translation to code slide.
118
-
119
- ### v0.11.0
120
-
121
- - Add `Slide#element` and `SlideContext#element` getters — expose the slide body DOM element directly for cases where `find()` is not sufficient, such as measuring dimensions, attaching event listeners, or integrating third-party libraries.
122
-
123
124
  ## See Also
124
125
 
125
126
  - [lively](https://github.com/socketry/lively) — The real-time application framework that powers Presently.
data/releases.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Releases
2
2
 
3
+ ## v0.21.0
4
+
5
+ ### Semantic Slides
6
+
7
+ Slides now use semantic Markdown instead of treating every heading as a named section. Use a single H1 for the slide title, write the body directly after it, and replace structural sections such as `# Title`, `# Body`, `# Subtitle`, and `# Heading` with ordinary title and body content. For example, replace `# Title` followed by the title text with `# The actual title`, and move any display title from front matter into the H1.
8
+
9
+ Template-specific placeholders are exact, case-sensitive H2 headings: `## Translation`, `## Caption`, `## Left`, and `## Right`. Custom templates should replace `self.section(name)` with `self.extract(name)` before rendering `self.document`; extraction removes that placeholder so it can be positioned independently.
10
+
11
+ ### Slide Scripts
12
+
13
+ Slide scripts are now exposed only through `Slide#scripts`. Replace the compatibility `Slide#script` accessor and `script:` initializer argument with the ordered `scripts` array.
14
+
15
+ ## v0.20.1
16
+
17
+ - Keep presenter toolbars within the viewport when slide paths are long.
18
+
3
19
  ## v0.19.0
4
20
 
5
21
  - Keep Anime.js resources created by delayed slide callbacks bound to the slide lifecycle.
data/templates/code.xrb CHANGED
@@ -1,17 +1,16 @@
1
+ <?r translation = self.extract("Translation") ?>
1
2
  <div class="slide-inner code-template">
2
- <?r if title = self.slide.title and !title.empty? ?>
3
- <div class="code-title"><p>#{title}</p></div>
4
- <?r end ?>
3
+ #{self.slide_header}
5
4
  <div class="code-viewport" data-focus-start="#{self.slide.focus&.first}" data-focus-end="#{self.slide.focus&.last}">
6
5
  <div class="code-scroll">
7
- #{self.section("body")}
6
+ #{self.document}
8
7
  </div>
9
8
  <div class="code-dim code-dim-top"></div>
10
9
  <div class="code-dim code-dim-bottom"></div>
11
10
  </div>
12
- <?r if self.section?("translation") ?>
11
+ <?r if translation ?>
13
12
  <div class="slide-translation">
14
- #{self.section("translation")}
13
+ #{translation}
15
14
  </div>
16
15
  <?r end ?>
17
16
  </div>
@@ -1,10 +1,12 @@
1
+ <?r translation = self.extract("Translation") ?>
1
2
  <div class="slide-inner default-template">
3
+ #{self.slide_header}
2
4
  <div class="slide-body">
3
- #{self.section("body")}
5
+ #{self.document}
4
6
  </div>
5
- <?r if self.section?("translation") ?>
7
+ <?r if translation ?>
6
8
  <div class="slide-translation">
7
- #{self.section("translation")}
9
+ #{translation}
8
10
  </div>
9
11
  <?r end ?>
10
12
  </div>
@@ -1,10 +1,12 @@
1
+ <?r translation = self.extract("Translation") ?>
1
2
  <div class="slide-inner diagram-template">
2
- <?r if self.section?("title") ?>
3
- <div class="slide-title">
4
- #{self.section("title")}
5
- </div>
6
- <?r end ?>
3
+ #{self.slide_header}
7
4
  <div class="slide-body">
8
- #{self.section("body")}
5
+ #{self.document}
9
6
  </div>
7
+ <?r if translation ?>
8
+ <div class="slide-translation">
9
+ #{translation}
10
+ </div>
11
+ <?r end ?>
10
12
  </div>
data/templates/fill.xrb CHANGED
@@ -1,5 +1,12 @@
1
+ <?r translation = self.extract("Translation") ?>
1
2
  <div class="slide-inner fill-template">
2
- <div class="slide-content">
3
- #{self.section("body")}
3
+ #{self.slide_header}
4
+ <div class="slide-body">
5
+ #{self.document}
4
6
  </div>
7
+ <?r if translation ?>
8
+ <div class="slide-translation">
9
+ #{translation}
10
+ </div>
11
+ <?r end ?>
5
12
  </div>
data/templates/image.xrb CHANGED
@@ -1,13 +1,16 @@
1
+ <?r caption = self.extract("Caption") ?>
2
+ <?r translation = self.extract("Translation") ?>
1
3
  <div class="slide-inner image-template">
4
+ #{self.slide_header}
2
5
  <div class="slide-image">
3
- #{self.section("body")}
6
+ #{self.document}
4
7
  </div>
5
- <?r if self.section?("caption") ?>
6
- <div class="slide-caption">#{self.section("caption")}</div>
8
+ <?r if caption ?>
9
+ <div class="slide-caption">#{caption}</div>
7
10
  <?r end ?>
8
- <?r if self.section?("translation") ?>
11
+ <?r if translation ?>
9
12
  <div class="slide-translation">
10
- #{self.section("translation")}
13
+ #{translation}
11
14
  </div>
12
15
  <?r end ?>
13
16
  </div>
@@ -1,10 +1,16 @@
1
+ <?r translation = self.extract("Translation") ?>
2
+ <?r header = self.slide_header ?>
3
+ <?r body = self.document ?>
1
4
  <div class="slide-inner section-template">
2
- <div class="slide-heading">
3
- #{self.section("heading")}
4
- </div>
5
- <?r if self.section?("translation") ?>
5
+ #{header}
6
+ <?r unless body.empty? ?>
7
+ <div class="slide-body">
8
+ #{body}
9
+ </div>
10
+ <?r end ?>
11
+ <?r if translation ?>
6
12
  <div class="slide-translation">
7
- #{self.section("translation")}
13
+ #{translation}
8
14
  </div>
9
15
  <?r end ?>
10
16
  </div>
@@ -1,10 +1,12 @@
1
+ <?r translation = self.extract("Translation") ?>
1
2
  <div class="slide-inner statement-template">
3
+ #{self.slide_header}
2
4
  <div class="slide-statement">
3
- #{self.section("body")}
5
+ #{self.document}
4
6
  </div>
5
- <?r if self.section?("translation") ?>
7
+ <?r if translation ?>
6
8
  <div class="slide-translation">
7
- #{self.section("translation")}
9
+ #{translation}
8
10
  </div>
9
11
  <?r end ?>
10
12
  </div>
data/templates/title.xrb CHANGED
@@ -1,15 +1,12 @@
1
+ <?r translation = self.extract("Translation") ?>
1
2
  <div class="slide-inner title-template">
2
- <div class="slide-title">
3
- #{self.section("title")}
3
+ #{self.slide_header}
4
+ <div class="slide-body">
5
+ #{self.document}
4
6
  </div>
5
- <?r if self.section?("subtitle") ?>
6
- <div class="slide-subtitle">
7
- #{self.section("subtitle")}
8
- </div>
9
- <?r end ?>
10
- <?r if self.section?("translation") ?>
7
+ <?r if translation ?>
11
8
  <div class="slide-translation">
12
- #{self.section("translation")}
9
+ #{translation}
13
10
  </div>
14
11
  <?r end ?>
15
12
  </div>
@@ -1,8 +1,26 @@
1
+ <?r left = self.extract("Left") ?>
2
+ <?r right = self.extract("Right") ?>
3
+ <?r translation = self.extract("Translation") ?>
4
+ <?r header = self.slide_header ?>
5
+ <?r body = self.document ?>
1
6
  <div class="slide-inner two-column-template">
2
- <div class="column left-column">
3
- #{self.section("left")}
4
- </div>
5
- <div class="column right-column">
6
- #{self.section("right")}
7
+ #{header}
8
+ <?r unless body.empty? ?>
9
+ <div class="slide-body">
10
+ #{body}
11
+ </div>
12
+ <?r end ?>
13
+ <div class="slide-columns">
14
+ <div class="column left-column">
15
+ #{left}
16
+ </div>
17
+ <div class="column right-column">
18
+ #{right}
19
+ </div>
7
20
  </div>
21
+ <?r if translation ?>
22
+ <div class="slide-translation">
23
+ #{translation}
24
+ </div>
25
+ <?r end ?>
8
26
  </div>
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.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -286,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
286
286
  - !ruby/object:Gem::Version
287
287
  version: '0'
288
288
  requirements: []
289
- rubygems_version: 4.0.10
289
+ rubygems_version: 4.0.16
290
290
  specification_version: 4
291
291
  summary: A web-based presentation tool built with Lively.
292
292
  test_files: []
metadata.gz.sig CHANGED
Binary file