presently 0.28.0 → 0.29.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: 19e0176d3b7e17bd06f07d79e3ae4379d1992f3b75beb87b7725e47a713961be
4
- data.tar.gz: 1b3bbd3f6a2c170f833a913cf1a0de5afb8030f3330773e62f1eba72153612c9
3
+ metadata.gz: 98c94c71298a2801441e3a2351c72e09b3c2e3dfdfde63a4d5b7673d42334aac
4
+ data.tar.gz: 15d88cf5bf1f4ddb3574c0f2ece5df062378650a1526bb14de74e20adb27354b
5
5
  SHA512:
6
- metadata.gz: 0e9e35182f5d1fcd2a4e0a625ebfe3d0f8d743ece09335a1a399c82b5f720138ef62044e070ca54c01995b685dd970956fe15600f3b2176aee318226c569030c
7
- data.tar.gz: d53ee079a8042e0abbcde56bb92d262084c2a8ea695dd4c41dec203bcd5d2a4050ba69557d2456cfcf5f85ddb48b8989bfe186938d8cda9ce6669e21d43cff99
6
+ metadata.gz: e0eb7788c6108fd1b16bfcac25deeeeb91c75e9af3e45f9b994351674931842880ce28567ccc30773e50d7a0f6d0c260f31e0db27d55a579121137e7a9ce912c
7
+ data.tar.gz: 8945b51b03abb5929c28267a544ed88e4f09b8baf342e33a2761f47d457e2476da320d3a7756319f0c2ebc8065ebe709d9b936c053a9d25c76eeffd533b67deb
checksums.yaml.gz.sig CHANGED
Binary file
@@ -112,6 +112,36 @@ Relative images embedded in Markdown resolve from the Markdown file's directory,
112
112
 
113
113
  Presently loads each discovered stylesheet once in deterministic presentation order. Directory styles are loaded from parent to child before the matching slide sidecar, so more specific styles naturally appear later in the cascade. The same stylesheets are used by the display, presenter, recorder, playback, and export interfaces.
114
114
 
115
+ ### Background Images
116
+
117
+ Add a background image behind any slide template using frontmatter:
118
+
119
+ ``` markdown
120
+ ---
121
+ template: title
122
+ background: images/mountains.jpg
123
+ background-size: cover
124
+ ---
125
+
126
+ # Welcome
127
+ ```
128
+
129
+ The background fills the available slide surface, including space outside the 16:9 content area. Content keeps its 16:9 layout. Images are centred and do not repeat.
130
+
131
+ Relative image paths resolve from the slide file's directory, just like Markdown images. Root-relative paths and external URLs are also supported.
132
+
133
+ `background-size` accepts:
134
+
135
+ | Value | Behaviour |
136
+ |---|---|
137
+ | `cover` (default) | Preserves proportions and fills the surface, cropping the image when necessary. |
138
+ | `contain` | Preserves proportions and shows the whole image, potentially leaving uncovered space. |
139
+ | `auto` | Uses the image's natural size. |
140
+
141
+ Missing or unrecognised sizes use `cover`. An explicit background image replaces the template's background image or gradient; uncovered space shows the underlying slide background colour. Slides without a background image retain their template's appearance.
142
+
143
+ Backgrounds appear in the display, presenter previews, recorder, playback, and PDF export. A preview or export with a different aspect ratio can crop the image differently from the audience display.
144
+
115
145
  ### Running the Presentation
116
146
 
117
147
  Start the server from your presentation directory:
@@ -272,24 +272,31 @@ module Presently
272
272
  # @parameter source_path [String] The Markdown source path.
273
273
  # @parameter root [String] The presentation asset root.
274
274
  def rewrite_image_urls!(document, source_path, root)
275
+ document.walk do |node|
276
+ node.url = resolve_image_url(node.url, source_path, root) if node.type == :image
277
+ end
278
+ end
279
+
280
+ # Resolve an image URL relative to its source file within the presentation.
281
+ # @parameter url [String] The image URL.
282
+ # @parameter source_path [String] The source file path.
283
+ # @parameter root [String] The presentation asset root.
284
+ # @returns [String] The resolved URL, preserving absolute URLs.
285
+ def resolve_image_url(url, source_path, root)
286
+ reference = Protocol::URL[url]
287
+ return url unless reference.is_a?(Protocol::URL::Relative)
288
+ return url if reference.path.empty? || reference.path.absolute?
289
+
275
290
  directory = File.dirname(File.expand_path(source_path))
276
291
  root = File.expand_path(root)
277
- return unless directory == root || directory.start_with?(root + File::SEPARATOR)
292
+ return url unless directory == root || directory.start_with?(root + File::SEPARATOR)
278
293
 
279
294
  relative_directory = directory.delete_prefix(root).delete_prefix(File::SEPARATOR)
280
295
  base_path = Stylesheet::PREFIX
281
296
  base_path += Stylesheet.encode_path(relative_directory) + "/" unless relative_directory.empty?
282
297
  base_url = Protocol::URL::Relative.new(base_path)
283
298
 
284
- document.walk do |node|
285
- next unless node.type == :image
286
-
287
- url = Protocol::URL[node.url]
288
- next unless url.is_a?(Protocol::URL::Relative)
289
- next if url.path.empty? || url.path.absolute?
290
-
291
- node.url = (base_url + url).to_s
292
- end
299
+ (base_url + reference).to_s
293
300
  end
294
301
 
295
302
  end
@@ -348,6 +355,22 @@ module Presently
348
355
  @front_matter&.fetch("template", "default") || "default"
349
356
  end
350
357
 
358
+ # The background image URL, resolved relative to this slide's source file.
359
+ # @returns [String | Nil] The URL, or `nil` when no image is specified.
360
+ def background
361
+ value = @front_matter&.fetch("background", nil)
362
+ return unless value.is_a?(String) && !value.strip.empty?
363
+
364
+ Parser.resolve_image_url(value, source_path, @presentation.root)
365
+ end
366
+
367
+ # How the background image is sized within the full slide surface.
368
+ # @returns [String] `"cover"`, `"contain"`, or `"auto"`, defaulting to `"cover"`.
369
+ def background_size
370
+ value = @front_matter&.fetch("background-size", nil)
371
+ ["cover", "contain", "auto"].include?(value) ? value : "cover"
372
+ end
373
+
351
374
  # The expected duration of this slide in seconds.
352
375
  # Negative, invalid, or non-finite values are treated as zero.
353
376
  # @returns [Float] A finite duration of at least `0.0`, defaulting to `0.0` when unspecified or null.
@@ -50,7 +50,14 @@ module Presently
50
50
  classes = classes.compact.join(" ")
51
51
  path = Stylesheet.encode_path(slide.path)
52
52
 
53
- builder.tag(:div, class: "slide-surface", data: {template: slide.template}) do
53
+ background = slide.background
54
+ if background
55
+ # Escape the CSS string independently of the builder's HTML escaping:
56
+ url = background.gsub(/["\\\x00-\x1f\x7f]/){|character| "\\#{character.ord.to_s(16)} "}
57
+ style = %(background-image: url("#{url}"); background-size: #{slide.background_size};)
58
+ end
59
+
60
+ builder.tag(:div, class: "slide-surface", data: {template: slide.template, background: background}, style: style) do
54
61
  builder.tag(:div, class: classes, data: {template: slide.template}, "data-slide-path": path) do
55
62
  builder.raw(html)
56
63
 
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Presently
8
- VERSION = "0.28.0"
8
+ VERSION = "0.29.0"
9
9
  end
@@ -4,8 +4,19 @@ import {runScript} from './Scripts.js';
4
4
 
5
5
  const SLIDE_CHANGE_EVENT = 'presently:slide:change';
6
6
 
7
- async function prepareSlides() {
8
- await Syntax.highlight();
7
+ // Wait for background images to decode before capturing a transition or export.
8
+ // A missing or invalid image must not prevent the slides from becoming ready.
9
+ export async function prepareSlideBackgrounds(root = document) {
10
+ const urls = new Set(Array.from(root.querySelectorAll('.slide-surface[data-background]'), surface => surface.dataset.background));
11
+ await Promise.all(Array.from(urls, async url => {
12
+ const image = new Image();
13
+ image.src = url;
14
+ await image.decode().catch(() => {});
15
+ }));
16
+ }
17
+
18
+ async function prepareSlides(view) {
19
+ await Promise.all([Syntax.highlight(), prepareSlideBackgrounds(view)]);
9
20
  await applyCodeFocus();
10
21
  }
11
22
 
@@ -42,7 +53,7 @@ export class SlideRendering {
42
53
  if (slide) this.#slides.push(slide);
43
54
  });
44
55
 
45
- await prepareSlides();
56
+ await prepareSlides(this.#view);
46
57
  return !this.#disposed;
47
58
  }
48
59
 
@@ -1,4 +1,4 @@
1
1
  export {Slide, SlideBuilder, SlideContext, SlideElements} from './Presently/Slide.js';
2
2
  export {applyCodeFocus} from './Presently/CodeFocus.js';
3
- export {SlideRendering} from './Presently/SlideRendering.js';
3
+ export {SlideRendering, prepareSlideBackgrounds} from './Presently/SlideRendering.js';
4
4
  export {runAllSlideScripts, runScript} from './Presently/Scripts.js';
@@ -17,6 +17,9 @@
17
17
  place-items: center;
18
18
  container-type: size;
19
19
  background: var(--slide-background);
20
+ background-size: cover;
21
+ background-position: center;
22
+ background-repeat: no-repeat;
20
23
  }
21
24
 
22
25
  .slide {
@@ -159,7 +162,7 @@
159
162
 
160
163
  /* Section template */
161
164
  .slide-surface[data-template="section"] {
162
- background: linear-gradient(135deg, var(--accent), var(--surface-light));
165
+ background-image: linear-gradient(135deg, var(--accent), var(--surface-light));
163
166
  }
164
167
 
165
168
  .section-template {
data/public/export.js CHANGED
@@ -1,4 +1,4 @@
1
- import {applyCodeFocus, runAllSlideScripts} from '@socketry/presently';
1
+ import {applyCodeFocus, runAllSlideScripts, prepareSlideBackgrounds} from '@socketry/presently';
2
2
  import Syntax from '@socketry/syntax';
3
3
 
4
4
  // Wait for two animation frames, ensuring the browser has processed all pending
@@ -9,15 +9,16 @@ function waitForRender() {
9
9
  }
10
10
 
11
11
  async function main() {
12
- // 1. Kick off syntax highlighting and font loading concurrently.
12
+ // 1. Kick off syntax highlighting, background decoding, and font loading concurrently.
13
13
  // Slide scripts are synchronous and don't depend on either, so run them now.
14
14
  const syntaxDone = Syntax.highlight();
15
+ const backgroundsDone = prepareSlideBackgrounds();
15
16
 
16
17
  // 2. Run slide scripts (synchronous in export mode — sets visibility instantly).
17
18
  runAllSlideScripts({animated: false});
18
19
 
19
- // 3. Wait for syntax and fonts to finish before applying focus.
20
- await Promise.all([syntaxDone, document.fonts.ready]);
20
+ // 3. Wait for syntax, backgrounds, and fonts to finish before applying focus.
21
+ await Promise.all([syntaxDone, backgroundsDone, document.fonts.ready]);
21
22
 
22
23
  // 4. Apply code focus now that syntax-code elements are ready.
23
24
  await applyCodeFocus();
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.28.0
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file