presently 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bake/presently/rehearse.rb +1 -9
- data/context/animated-diagrams.md +1 -1
- data/context/getting-started.md +37 -29
- data/lib/presently/application.rb +7 -6
- data/lib/presently/export.xrb +2 -0
- data/lib/presently/page.rb +15 -3
- data/lib/presently/playback.xrb +2 -1
- data/lib/presently/slide.rb +101 -50
- data/lib/presently/slide_renderer.rb +55 -16
- data/lib/presently/templates.rb +1 -1
- data/lib/presently/version.rb +1 -1
- data/public/_static/custom.css +7 -74
- data/public/_static/display.css +25 -0
- data/public/_static/export.css +111 -0
- data/public/_static/home.css +97 -0
- data/public/_static/index.css +4 -1131
- data/public/_static/playback.css +115 -0
- data/public/_static/presenter.css +137 -0
- data/public/_static/recorder.css +161 -0
- data/public/_static/slides.css +538 -0
- data/readme.md +10 -8
- data/releases.md +21 -0
- data/templates/code.xrb +5 -6
- data/templates/default.xrb +5 -3
- data/templates/diagram.xrb +8 -6
- data/templates/fill.xrb +9 -2
- data/templates/image.xrb +8 -5
- data/templates/section.xrb +11 -5
- data/templates/statement.xrb +5 -3
- data/templates/title.xrb +6 -9
- data/templates/two_column.xrb +23 -5
- data.tar.gz.sig +0 -0
- metadata +9 -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: bceb99c19c4563f60e51fe3e17cce836c7176ebd9b695919af5737ccc87fd4ca
|
|
4
|
+
data.tar.gz: 102b2bbab9019d17c5aadb85c3f40044c1e94bdb2e587482c246d0c184f5fa36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8edf0ac3857670ce3afe65522bdf9d560b2f909fb68a85a43161200047f6a8fe588be5bef3ee685ca007a927d47999b5afc7c0db65ae43896c78b018ec347ffc
|
|
7
|
+
data.tar.gz: b8cca4183e4494e9fdd2923e429bc3bdca75310f127eba5474f410214c90f2cd5a0b2d89bb03df1e887673d30b81ebb45320b3af5853b73f137e6fbe6c2554f7
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/bake/presently/rehearse.rb
CHANGED
|
@@ -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 =
|
|
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]}
|
|
@@ -192,7 +192,7 @@ Use semantic classes and CSS custom properties so that meaning remains separate
|
|
|
192
192
|
.diagram-node {
|
|
193
193
|
--node-color: var(--accent);
|
|
194
194
|
border: 0.08em solid var(--node-color);
|
|
195
|
-
background: color-mix(in srgb, var(--node-color) 10%, var(--slide-
|
|
195
|
+
background: color-mix(in srgb, var(--node-color) 10%, var(--slide-background));
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
.diagram-node[data-tone="storage"] {
|
data/context/getting-started.md
CHANGED
|
@@ -60,11 +60,7 @@ template: title
|
|
|
60
60
|
duration: 30
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
-
#
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
235
|
+
# Part Two
|
|
242
236
|
|
|
243
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
310
|
+
## Translation
|
|
312
311
|
|
|
313
312
|
未来を予測する最善の方法は、それを創ることである。
|
|
314
313
|
```
|
|
315
314
|
|
|
316
315
|
### Translations
|
|
317
316
|
|
|
318
|
-
|
|
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
|

|
|
331
330
|
|
|
332
|
-
|
|
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
|
-
#
|
|
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}
|
|
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
|
|
|
@@ -72,10 +72,11 @@ module Presently
|
|
|
72
72
|
|
|
73
73
|
# Create a Presently page with the presentation-specific stylesheets.
|
|
74
74
|
# @parameter view [Live::View] The root view for the page.
|
|
75
|
+
# @parameter interface [Symbol | Nil] The interface-specific stylesheet to load.
|
|
75
76
|
# @returns [Page] The presentation page.
|
|
76
|
-
def make_page(view)
|
|
77
|
+
def make_page(view, interface: nil)
|
|
77
78
|
stylesheets = controller.presentation.stylesheets.map(&:url)
|
|
78
|
-
Page.new(title: title, body: view, stylesheets: stylesheets)
|
|
79
|
+
Page.new(title: title, body: view, interface: interface, stylesheets: stylesheets)
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
# Add Presently's application routes.
|
|
@@ -83,12 +84,12 @@ module Presently
|
|
|
83
84
|
def configure_routes(router)
|
|
84
85
|
router.get("/") do
|
|
85
86
|
body = resolver.root(HomeView)
|
|
86
|
-
Page.new(title: title, body: body).call
|
|
87
|
+
Page.new(title: title, body: body, interface: :home).call
|
|
87
88
|
end
|
|
88
89
|
|
|
89
|
-
router.get("/display"){make_page(resolver.root(DisplayView)).call}
|
|
90
|
-
router.get("/presenter"){make_page(resolver.root(PresenterView)).call}
|
|
91
|
-
router.get("/record"){make_page(resolver.root(RecordingView)).call}
|
|
90
|
+
router.get("/display"){make_page(resolver.root(DisplayView), interface: :display).call}
|
|
91
|
+
router.get("/presenter"){make_page(resolver.root(PresenterView), interface: :presenter).call}
|
|
92
|
+
router.get("/record"){make_page(resolver.root(RecordingView), interface: :recorder).call}
|
|
92
93
|
|
|
93
94
|
router.route("/recordings", methods: ["GET", "HEAD", "PUT"]) do |request|
|
|
94
95
|
handle_recording(request, request_parameters(request))
|
data/lib/presently/export.xrb
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
|
|
6
6
|
<link rel="stylesheet" href="/_static/index.css" type="text/css" />
|
|
7
|
+
<link rel="stylesheet" href="/_static/slides.css" type="text/css" />
|
|
8
|
+
<link rel="stylesheet" href="/_static/export.css" type="text/css" />
|
|
7
9
|
<link rel="stylesheet" href="/_static/custom.css" type="text/css" />
|
|
8
10
|
<link rel="stylesheet" href="/_components/@socketry/syntax/themes/base/syntax.css" type="text/css" />
|
|
9
11
|
<?r self.stylesheets.each do |stylesheet| ?>
|
data/lib/presently/page.rb
CHANGED
|
@@ -13,8 +13,17 @@ module Presently
|
|
|
13
13
|
class Page < Lively::Page
|
|
14
14
|
ICON = "/_static/icon.png"
|
|
15
15
|
STYLESHEETS = [
|
|
16
|
-
{href: "/_static/site.css", media: "screen"}.freeze,
|
|
17
16
|
{href: "/_static/index.css", media: "screen"}.freeze,
|
|
17
|
+
].freeze
|
|
18
|
+
SLIDES_STYLESHEET = {href: "/_static/slides.css", media: "screen"}.freeze
|
|
19
|
+
INTERFACE_STYLESHEETS = {
|
|
20
|
+
home: [{href: "/_static/home.css", media: "screen"}.freeze].freeze,
|
|
21
|
+
display: [SLIDES_STYLESHEET, {href: "/_static/display.css", media: "screen"}.freeze].freeze,
|
|
22
|
+
presenter: [SLIDES_STYLESHEET, {href: "/_static/presenter.css", media: "screen"}.freeze].freeze,
|
|
23
|
+
recorder: [SLIDES_STYLESHEET, {href: "/_static/recorder.css", media: "screen"}.freeze].freeze,
|
|
24
|
+
}.freeze
|
|
25
|
+
DEFAULT_INTERFACE_STYLESHEETS = INTERFACE_STYLESHEETS.values.flatten.uniq.freeze
|
|
26
|
+
TRAILING_STYLESHEETS = [
|
|
18
27
|
{href: "/_static/custom.css", media: "screen"}.freeze,
|
|
19
28
|
{href: "/_components/@socketry/syntax/themes/base/syntax.css", media: "screen"}.freeze,
|
|
20
29
|
].freeze
|
|
@@ -31,13 +40,16 @@ module Presently
|
|
|
31
40
|
# Initialize a new page.
|
|
32
41
|
# @parameter title [String] The page title.
|
|
33
42
|
# @parameter body [Live::View | Nil] The Live view to embed in the page.
|
|
43
|
+
# @parameter interface [Symbol | Nil] The interface-specific stylesheets to load, or all interface stylesheets by default for compatibility.
|
|
34
44
|
# @parameter stylesheets [Array(String | Hash)] Presentation-specific stylesheets.
|
|
35
|
-
def initialize(title: "Presently", body: nil, stylesheets: [])
|
|
45
|
+
def initialize(title: "Presently", body: nil, interface: nil, stylesheets: [])
|
|
46
|
+
interface_stylesheets = interface ? INTERFACE_STYLESHEETS.fetch(interface) : DEFAULT_INTERFACE_STYLESHEETS
|
|
47
|
+
|
|
36
48
|
super(
|
|
37
49
|
title: title,
|
|
38
50
|
body: body,
|
|
39
51
|
icon: ICON,
|
|
40
|
-
stylesheets: STYLESHEETS + stylesheets,
|
|
52
|
+
stylesheets: STYLESHEETS + interface_stylesheets + TRAILING_STYLESHEETS + stylesheets,
|
|
41
53
|
imports: IMPORTS,
|
|
42
54
|
modules: MODULES,
|
|
43
55
|
)
|
data/lib/presently/playback.xrb
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
8
|
|
|
9
9
|
<link rel="icon" type="image/png" href="/_static/icon.png" />
|
|
10
|
-
<link rel="stylesheet" href="/_static/site.css" type="text/css" media="screen" />
|
|
11
10
|
<link rel="stylesheet" href="/_static/index.css" type="text/css" media="screen" />
|
|
11
|
+
<link rel="stylesheet" href="/_static/slides.css" type="text/css" media="screen" />
|
|
12
|
+
<link rel="stylesheet" href="/_static/playback.css" type="text/css" media="screen" />
|
|
12
13
|
<link rel="stylesheet" href="/_static/custom.css" type="text/css" media="screen" />
|
|
13
14
|
<link rel="stylesheet" href="/_components/@socketry/syntax/themes/base/syntax.css" type="text/css" media="screen" />
|
|
14
15
|
<?r self.stylesheets.each do |stylesheet| ?>
|
data/lib/presently/slide.rb
CHANGED
|
@@ -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),
|
|
18
|
-
#
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
@
|
|
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 [
|
|
283
|
-
attr :
|
|
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
|
|
357
|
+
# @returns [String] The H1 text, front matter title, or filename without extension.
|
|
313
358
|
def title
|
|
314
|
-
@front_matter&.fetch("title",
|
|
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
|
|
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
|
-
#
|
|
79
|
-
# @returns [
|
|
80
|
-
def
|
|
81
|
-
@
|
|
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
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|