jekyll-slides 0.1.0 → 0.3.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
- data/CHANGELOG.md +14 -1
- data/README.md +80 -4
- data/assets/css/presentation.css +1 -1
- data/doc/CHANGELOG.md +14 -1
- data/doc/Jekyll/Slides/Deck.md +40 -0
- data/doc/Jekyll/Slides/DeckAssembler.md +17 -0
- data/doc/Jekyll/Slides/FrontMatter.md +16 -0
- data/doc/Jekyll/Slides/JekyllIntegration.md +0 -3
- data/doc/Jekyll/Slides/SlideFile.md +18 -0
- data/doc/Jekyll/Slides/SlideMetadata.md +6 -0
- data/doc/Jekyll/Slides/Support.md +6 -0
- data/doc/Jekyll/Slides.md +15 -4
- data/doc/Jekyll.md +1 -1
- data/doc/README.md +80 -4
- data/doc/index.csv +19 -1
- data/lib/jekyll/slides/component_renderer.rb +33 -5
- data/lib/jekyll/slides/deck.rb +151 -0
- data/lib/jekyll/slides/deck_assembler.rb +147 -0
- data/lib/jekyll/slides/front_matter.rb +39 -0
- data/lib/jekyll/slides/jekyll_integration.rb +3 -3
- data/lib/jekyll/slides/renderer.rb +14 -0
- data/lib/jekyll/slides/slide_file.rb +55 -0
- data/lib/jekyll/slides/slide_metadata.rb +26 -18
- data/lib/jekyll/slides/support.rb +14 -0
- data/lib/jekyll/slides/version.rb +1 -1
- data/lib/jekyll/slides.rb +14 -2
- data/{llm.txt → llms.txt} +15 -4
- metadata +10 -2
data/doc/README.md
CHANGED
|
@@ -59,6 +59,62 @@ class: keynote
|
|
|
59
59
|
-->
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
## Deck folders
|
|
63
|
+
|
|
64
|
+
A deck can also be a folder of Markdown files, one file per slide, instead of one file with `---` separators:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
talks/my-talk/
|
|
68
|
+
index.md # deck front matter, opts in with `slides: true`
|
|
69
|
+
01-title.md
|
|
70
|
+
02-the-idea.md
|
|
71
|
+
03-code.md
|
|
72
|
+
diagram.png # ordinary static file, untouched
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The entry file must be named `index.md` and carry the deck's front matter, `layout: presentation`, and `slides: true`:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
# talks/my-talk/index.md
|
|
79
|
+
---
|
|
80
|
+
layout: presentation
|
|
81
|
+
title: My talk
|
|
82
|
+
slides: true
|
|
83
|
+
---
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`slides: true` discovers every other Markdown file that is a direct child of the folder (not recursive), skipping names starting with `_` or `.`, and skipping anything Jekyll itself withholds: a file dropped by the site's `exclude:` setting, or one whose front matter sets `published: false`. Naming a withheld file in an explicit list warns and skips it too, so a deck never publishes content the site holds back. Files are ordered by a natural sort of the filename: digit runs compare numerically and sort before letters, so `01-title.md`, `02-the-idea.md`, `10-outro.md`, then any file with no numeric prefix, sorted alphabetically. Each file holds one slide, or several separated by `---`, exactly as in a single-file deck. Body content in `index.md`, if any, becomes the deck's leading slides.
|
|
87
|
+
|
|
88
|
+
A slide file can carry its own YAML front matter with `layout`, `background`, and `class`, as an alternative to the `<!-- ... -->` comment:
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
---
|
|
92
|
+
layout: statement
|
|
93
|
+
background: spotlight
|
|
94
|
+
---
|
|
95
|
+
# A bold claim
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Front matter in the file applies to every slide it holds; a slide's own comment overrides it key by key.
|
|
99
|
+
|
|
100
|
+
Set `slides:` to a list instead of `true` to turn the entry file into a readable running order, in presentation order:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
---
|
|
104
|
+
layout: presentation
|
|
105
|
+
title: My talk
|
|
106
|
+
slides:
|
|
107
|
+
- 01-title.md
|
|
108
|
+
- 02-the-idea.md
|
|
109
|
+
# - 03-detour.md
|
|
110
|
+
- 04-code.md
|
|
111
|
+
---
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Cutting a slide for a shorter time slot is commenting out one line, not moving a file out of the folder.
|
|
115
|
+
|
|
116
|
+
`slides:` is what opts a folder in. Opting in absorbs the whole folder: every Markdown file it absorbs is removed from the site's pages, collection documents, and static files, so none of them is ever published on its own. This holds even for a file commented out of an explicit list; the list controls order and inclusion in the deck, not what gets absorbed. Without `slides:`, a folder holding an `index.md` presentation behaves exactly as it does today, and sibling files are left alone. This matters if the site applies `layout: presentation` through a broad `defaults:` rule, since that alone never triggers absorption.
|
|
117
|
+
|
|
62
118
|
## Editors and terminals
|
|
63
119
|
|
|
64
120
|
Put a kramdown inline attribute list immediately after a fenced block. Attribute values must be quoted; class and ID tokens such as `.editor` and `#order-total` do not need quotes. `.editor` renders a syntax-highlighted editor window; `.terminal` renders a terminal window.
|
|
@@ -80,7 +136,7 @@ ruby 3.4.1
|
|
|
80
136
|
{: .terminal #ruby-version title="terminal" size="sm"}
|
|
81
137
|
````
|
|
82
138
|
|
|
83
|
-
Editor/terminal attributes are `title`, `id` (or `#id`), `size` (`sm`, `md`, `lg`), `line_numbers` (`true`/`false`), `focus`, and `highlight`. Ranges accept comma-separated lines and ranges such as `2,4-6,9`; invalid values fall back safely with a Jekyll warning. Terminal prompts beginning with `$ `, `> `, or `❯ ` are styled separately. Source remains server-rendered and copyable.
|
|
139
|
+
Editor/terminal attributes are `title`, `id` (or `#id`), `size` (`sm`, `md`, `lg`), `theme`, `line_numbers` (`true`/`false`), `focus`, and `highlight`. Ranges accept comma-separated lines and ranges such as `2,4-6,9`; invalid values fall back safely with a Jekyll warning. Terminal prompts beginning with `$ `, `> `, or `❯ ` are styled separately. Source remains server-rendered and copyable.
|
|
84
140
|
|
|
85
141
|
Long editor and terminal blocks scroll within the slide. Blocks over 20 lines produce an advisory build warning; the amount that fits depends on the layout and font size. Split long examples across slides for print, where scrolling is unavailable.
|
|
86
142
|
|
|
@@ -90,7 +146,7 @@ Code remains selectable and copyable with normal browser controls; version 0.1 d
|
|
|
90
146
|
|
|
91
147
|
## Themes and configuration
|
|
92
148
|
|
|
93
|
-
Presentation themes are `minimal-light`, `minimal-dark`, `midnight` (the default), and `
|
|
149
|
+
Presentation themes are `minimal-light`, `minimal-dark`, `midnight` (the default), `ruby`, and the four Catppuccin flavors `catppuccin-latte`, `catppuccin-frappe`, `catppuccin-macchiato`, and `catppuccin-mocha`. These style the slides without replacing the site's Jekyll theme. Configure defaults under `slides:`:
|
|
94
150
|
|
|
95
151
|
```yaml
|
|
96
152
|
slides:
|
|
@@ -106,11 +162,31 @@ The same keys can be overridden per presentation in front matter (`theme`, `aspe
|
|
|
106
162
|
|
|
107
163
|
For a light presentation, set `slides: { theme: minimal-light }` in `_config.yml`, or `theme: minimal-light` in the deck's front matter. The [readability example](https://github.com/lucianghinda/jekyll-slides/blob/main/examples/readability.md) demonstrates the light theme with code, terminal output, tables, and prose.
|
|
108
164
|
|
|
165
|
+
### Terminal themes
|
|
166
|
+
|
|
167
|
+
The Catppuccin flavors are ported from the palette shared by [catppuccin/ghostty](https://github.com/catppuccin/ghostty). Hues are the published values; lightness is adjusted only where a token cannot otherwise clear the 4.5:1 contrast the project checks on every code surface. Latte, the light flavor, needed the most adjustment.
|
|
168
|
+
|
|
169
|
+
Any theme also works on a single editor or terminal block through a `theme` attribute, so one window can carry a different palette from the deck around it:
|
|
170
|
+
|
|
171
|
+
````markdown
|
|
172
|
+
```console
|
|
173
|
+
$ ghostty --version
|
|
174
|
+
ghostty 1.0.1
|
|
175
|
+
```
|
|
176
|
+
{: .terminal title="ghostty" theme="catppuccin-latte"}
|
|
177
|
+
````
|
|
178
|
+
|
|
179
|
+
The override repaints that window completely: surface, title bar, syntax colors, prompt, and body text. Everything outside it keeps the deck theme. An unknown name warns during the build and falls back to the deck theme. The [terminal themes example](https://github.com/lucianghinda/jekyll-slides/blob/main/examples/terminal-themes.md) shows a deck-wide flavor next to per-window overrides.
|
|
180
|
+
|
|
181
|
+
### Window chrome
|
|
182
|
+
|
|
183
|
+
Editors and terminals render the same macOS-style title bar: traffic lights on the left, the `title` centered in the bar, and for editors the language on the right. The traffic lights keep the macOS red, amber, and green in every theme, the way a real title bar does; only the window surface follows the theme. Bar height, control size, and spacing scale with the component `size`.
|
|
184
|
+
|
|
109
185
|
## Typography and readability
|
|
110
186
|
|
|
111
187
|
The gem bundles Atkinson Hyperlegible Next for text and Atkinson Hyperlegible Mono for code, including variable upright and italic faces. Fonts load from the site's own assets and work offline. The font files retain their SIL Open Font License; source revisions, checksums, and notices are in [assets/fonts](https://github.com/lucianghinda/jekyll-slides/blob/main/assets/fonts/README.md).
|
|
112
188
|
|
|
113
|
-
On the 1920×1080 slide canvas, body text is 44px, section headings are 72px, and code sizes `sm`, `md`, and `lg` are 32px, 38px, and 42px. These sizes scale with the slide. Filenames and line numbers remain readable, and focused lines use borders and background color while keeping surrounding code fully visible. All
|
|
189
|
+
On the 1920×1080 slide canvas, body text is 44px, section headings are 72px, and code sizes `sm`, `md`, and `lg` are 32px, 38px, and 42px. These sizes scale with the slide. Filenames and line numbers remain readable, and focused lines use borders and background color while keeping surrounding code fully visible. All eight themes check syntax-token contrast against normal, highlighted, and focused code surfaces, both deck-wide and as a single-window override.
|
|
114
190
|
|
|
115
191
|
Prefer medium or large code and short examples for projection. Long blocks can scroll, but an audience cannot reveal hidden lines independently; split examples when presenting or printing. Screen contrast tests do not replace checking the actual projector and viewing distance.
|
|
116
192
|
|
|
@@ -151,7 +227,7 @@ bundle exec rake docs
|
|
|
151
227
|
```
|
|
152
228
|
|
|
153
229
|
This replaces `doc/` with fresh YARD Markdown, updates the documentation index in
|
|
154
|
-
`doc/Jekyll/Slides.md`, and writes `
|
|
230
|
+
`doc/Jekyll/Slides.md`, and writes `llms.txt` with links relative to the gem root.
|
|
155
231
|
The generated files are checked in and shipped with the gem. Internal plans,
|
|
156
232
|
temporary files, and Node dependencies are excluded. YARD and `yard-markdown`
|
|
157
233
|
are development tools only.
|
data/doc/index.csv
CHANGED
|
@@ -5,27 +5,44 @@ Jekyll,Module,Jekyll.md
|
|
|
5
5
|
Jekyll::Slides,Module,Jekyll/Slides.md
|
|
6
6
|
Jekyll::Slides.ROOT,Constant,Jekyll/Slides.md#constant-ROOT
|
|
7
7
|
Jekyll::Slides.LAYOUT_NAME,Constant,Jekyll/Slides.md#constant-LAYOUT_NAME
|
|
8
|
+
Jekyll::Slides.THEMES,Constant,Jekyll/Slides.md#constant-THEMES
|
|
9
|
+
Jekyll::Slides.DEFAULT_THEME,Constant,Jekyll/Slides.md#constant-DEFAULT_THEME
|
|
8
10
|
Jekyll::Slides.VERSION,Constant,Jekyll/Slides.md#constant-VERSION
|
|
9
11
|
Jekyll::Slides::Assets,Module,Jekyll/Slides/Assets.md
|
|
10
12
|
Jekyll::Slides::Assets.asset_files,Method,Jekyll/Slides/Assets.md#method-c-asset_files
|
|
11
13
|
Jekyll::Slides::Assets.install,Method,Jekyll/Slides/Assets.md#method-c-install
|
|
12
14
|
Jekyll::Slides::Support,Module,Jekyll/Slides/Support.md
|
|
13
15
|
Jekyll::Slides::Support.escape,Method,Jekyll/Slides/Support.md#method-c-escape
|
|
16
|
+
Jekyll::Slides::Support.natural_sort_key,Method,Jekyll/Slides/Support.md#method-c-natural_sort_key
|
|
14
17
|
Jekyll::Slides::Support.warn,Method,Jekyll/Slides/Support.md#method-c-warn
|
|
18
|
+
Jekyll::Slides::FrontMatter,Module,Jekyll/Slides/FrontMatter.md
|
|
19
|
+
Jekyll::Slides::FrontMatter.parse,Method,Jekyll/Slides/FrontMatter.md#method-c-parse
|
|
20
|
+
Jekyll::Slides::FrontMatter.split,Method,Jekyll/Slides/FrontMatter.md#method-c-split
|
|
21
|
+
Jekyll::Slides::DeckAssembler,Module,Jekyll/Slides/DeckAssembler.md
|
|
22
|
+
Jekyll::Slides::DeckAssembler.assemble,Method,Jekyll/Slides/DeckAssembler.md#method-c-assemble
|
|
15
23
|
Jekyll::Slides::JekyllIntegration,Module,Jekyll/Slides/JekyllIntegration.md
|
|
16
24
|
Jekyll::Slides::JekyllIntegration.DEFAULTS,Constant,Jekyll/Slides/JekyllIntegration.md#constant-DEFAULTS
|
|
17
|
-
Jekyll::Slides::JekyllIntegration.THEMES,Constant,Jekyll/Slides/JekyllIntegration.md#constant-THEMES
|
|
18
25
|
Jekyll::Slides::JekyllIntegration.BOOLEAN_OPTIONS,Constant,Jekyll/Slides/JekyllIntegration.md#constant-BOOLEAN_OPTIONS
|
|
19
26
|
Jekyll::Slides::JekyllIntegration.OPTION_KEYS,Constant,Jekyll/Slides/JekyllIntegration.md#constant-OPTION_KEYS
|
|
20
27
|
Jekyll::Slides::JekyllIntegration.install!,Method,Jekyll/Slides/JekyllIntegration.md#method-c-install-21
|
|
21
28
|
Jekyll::Slides::JekyllIntegration.process,Method,Jekyll/Slides/JekyllIntegration.md#method-c-process
|
|
22
29
|
Jekyll::Slides::JekyllIntegration.process_site,Method,Jekyll/Slides/JekyllIntegration.md#method-c-process_site
|
|
30
|
+
Jekyll::Slides::Deck,Class,Jekyll/Slides/Deck.md
|
|
31
|
+
Jekyll::Slides::Deck.OPTION_KEYS,Constant,Jekyll/Slides/Deck.md#constant-OPTION_KEYS
|
|
32
|
+
Jekyll::Slides::Deck.consumed_paths,Method,Jekyll/Slides/Deck.md#method-i-consumed_paths
|
|
33
|
+
Jekyll::Slides::Deck.content,Method,Jekyll/Slides/Deck.md#method-i-content
|
|
34
|
+
Jekyll::Slides::Deck.deck?,Method,Jekyll/Slides/Deck.md#method-i-deck-3F
|
|
35
|
+
Jekyll::Slides::Deck.initialize,Method,Jekyll/Slides/Deck.md#method-i-initialize
|
|
36
|
+
Jekyll::Slides::Deck.slide_paths,Method,Jekyll/Slides/Deck.md#method-i-slide_paths
|
|
23
37
|
Jekyll::Slides::Assets::SiteAssetFile,Class,Jekyll/Slides/Assets/SiteAssetFile.md
|
|
24
38
|
Jekyll::Slides::Assets::SiteAssetFile.write,Method,Jekyll/Slides/Assets/SiteAssetFile.md#method-i-write
|
|
25
39
|
Jekyll::Slides::Layout,Class,Jekyll/Slides/Layout.md
|
|
26
40
|
Jekyll::Slides::Layout.initialize,Method,Jekyll/Slides/Layout.md#method-i-initialize
|
|
27
41
|
Jekyll::Slides::Renderer,Class,Jekyll/Slides/Renderer.md
|
|
28
42
|
Jekyll::Slides::Renderer.convert,Method,Jekyll/Slides/Renderer.md#method-i-convert
|
|
43
|
+
Jekyll::Slides::SlideFile,Class,Jekyll/Slides/SlideFile.md
|
|
44
|
+
Jekyll::Slides::SlideFile.initialize,Method,Jekyll/Slides/SlideFile.md#method-i-initialize
|
|
45
|
+
Jekyll::Slides::SlideFile.slides,Method,Jekyll/Slides/SlideFile.md#method-i-slides
|
|
29
46
|
Jekyll::Slides::Presentation,Class,Jekyll/Slides/Presentation.md
|
|
30
47
|
Jekyll::Slides::Presentation.initialize,Method,Jekyll/Slides/Presentation.md#method-i-initialize
|
|
31
48
|
Jekyll::Slides::Presentation.render,Method,Jekyll/Slides/Presentation.md#method-i-render
|
|
@@ -45,6 +62,7 @@ Jekyll::Slides::SlideMetadata.KEYS,Constant,Jekyll/Slides/SlideMetadata.md#const
|
|
|
45
62
|
Jekyll::Slides::SlideMetadata.initialize,Method,Jekyll/Slides/SlideMetadata.md#method-i-initialize
|
|
46
63
|
Jekyll::Slides::SlideMetadata.parse,Method,Jekyll/Slides/SlideMetadata.md#method-i-parse
|
|
47
64
|
Jekyll::Slides::SlideMetadata.parse,Method,Jekyll/Slides/SlideMetadata.md#method-c-parse
|
|
65
|
+
Jekyll::Slides::SlideMetadata.split_comment,Method,Jekyll/Slides/SlideMetadata.md#method-c-split_comment
|
|
48
66
|
Jekyll::Slides::SlideMetadata.background,Attribute,Jekyll/Slides/SlideMetadata.md#attribute-i-background
|
|
49
67
|
Jekyll::Slides::SlideMetadata.content,Attribute,Jekyll/Slides/SlideMetadata.md#attribute-i-content
|
|
50
68
|
Jekyll::Slides::SlideMetadata.custom_class,Attribute,Jekyll/Slides/SlideMetadata.md#attribute-i-custom_class
|
|
@@ -165,9 +165,8 @@ module Jekyll
|
|
|
165
165
|
line_numbers = line_numbers?(attrs.fetch("line_numbers", "true"))
|
|
166
166
|
classes = ["code-window", "size-#{size}"] + attrs[:classes].reject { |name| name == "editor" }
|
|
167
167
|
classes << "has-focus" unless focus.empty?
|
|
168
|
-
attributes =
|
|
169
|
-
header =
|
|
170
|
-
header << %(<span class="code-window-language">#{Support.escape(language)}</span></header>)
|
|
168
|
+
attributes = window_attributes(classes, attrs)
|
|
169
|
+
header = window_header(language: language)
|
|
171
170
|
caption = attrs["title"] ? %(<figcaption>#{Support.escape(attrs['title'])}</figcaption>) : ""
|
|
172
171
|
source = lines.map { |line, newline| line + newline }.join
|
|
173
172
|
highlighted = @rouge_renderer.render_lines(source, language)
|
|
@@ -187,7 +186,7 @@ module Jekyll
|
|
|
187
186
|
def render_terminal(lines, attrs)
|
|
188
187
|
size = normalize_size(attrs["size"])
|
|
189
188
|
classes = ["terminal-window", "size-#{size}"] + attrs[:classes].reject { |name| name == "terminal" }
|
|
190
|
-
figure_attrs =
|
|
189
|
+
figure_attrs = window_attributes(classes, attrs)
|
|
191
190
|
caption = attrs["title"] ? %(<figcaption>#{Support.escape(attrs['title'])}</figcaption>) : ""
|
|
192
191
|
body = lines.map do |line, newline|
|
|
193
192
|
visible = Support.escape(line)
|
|
@@ -198,7 +197,36 @@ module Jekyll
|
|
|
198
197
|
end
|
|
199
198
|
%(<span class="tline">#{visible}#{Support.escape(newline)}</span>)
|
|
200
199
|
end.join
|
|
201
|
-
%(<figure#{figure_attrs}>#{caption}<pre><code>#{body}</code></pre></figure>)
|
|
200
|
+
%(<figure#{figure_attrs}>#{caption}#{window_header}<pre><code>#{body}</code></pre></figure>)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The macOS-style title bar both window types share: traffic lights on
|
|
204
|
+
# the left, and for editors the language on the right. The centered title
|
|
205
|
+
# is the figure's own caption, which stays a direct child of the figure so
|
|
206
|
+
# the markup remains a valid figure/figcaption pair.
|
|
207
|
+
def window_header(language: nil)
|
|
208
|
+
header = +'<header class="code-window-header"><span class="code-window-controls" aria-hidden="true"><i></i><i></i><i></i></span>'
|
|
209
|
+
header << %(<span class="code-window-language">#{Support.escape(language)}</span>) if language
|
|
210
|
+
header << "</header>"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def window_attributes(classes, attrs)
|
|
214
|
+
html_attributes(
|
|
215
|
+
"class" => classes.join(" "),
|
|
216
|
+
"id" => attrs[:id],
|
|
217
|
+
"data-code-theme" => component_theme(attrs["theme"])
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# A component may carry any deck theme, so one window can show a light
|
|
222
|
+
# terminal inside a dark deck. Returns nil to inherit the deck theme.
|
|
223
|
+
def component_theme(value)
|
|
224
|
+
return nil if value.nil? || value.to_s.empty?
|
|
225
|
+
return value.to_s if THEMES.include?(value.to_s)
|
|
226
|
+
|
|
227
|
+
Support.warn(@warning, "Unsupported component theme #{value.inspect}; expected one of #{THEMES.join(', ')}; " \
|
|
228
|
+
"using the deck theme")
|
|
229
|
+
nil
|
|
202
230
|
end
|
|
203
231
|
|
|
204
232
|
def line_range(value, max_line: nil)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Slides
|
|
5
|
+
# Represents one folder deck: an entry file plus the slide files a
|
|
6
|
+
# directory holds. Reads slide files from disk and assembles them, with
|
|
7
|
+
# the entry body, into one Markdown string ready for the existing
|
|
8
|
+
# +PresentationParser+ pipeline. Knows nothing about Jekyll page objects.
|
|
9
|
+
class Deck
|
|
10
|
+
# Top-level presentation options an author might mistakenly nest under
|
|
11
|
+
# +slides:+ instead of setting directly in front matter.
|
|
12
|
+
OPTION_KEYS = %w[theme progress slide_numbers overview fullscreen aspect_ratio].freeze
|
|
13
|
+
|
|
14
|
+
# rubocop:disable Metrics/ParameterLists -- this exact keyword set is the deck's public
|
|
15
|
+
# construction contract; the Jekyll integration layer wires it verbatim.
|
|
16
|
+
def initialize(directory:, entry_path:, entry_body:, slides:, markdown_extensions:, warning: nil, read_options: {}, published: nil)
|
|
17
|
+
@directory = directory
|
|
18
|
+
@entry_path = File.expand_path(entry_path)
|
|
19
|
+
@entry_body = entry_body.to_s
|
|
20
|
+
@slides_option = slides
|
|
21
|
+
@markdown_extensions = Array(markdown_extensions).map { |ext| ext.to_s.downcase }
|
|
22
|
+
@warning = warning
|
|
23
|
+
@read_options = read_options
|
|
24
|
+
@published = published
|
|
25
|
+
@deck = resolve_deck?
|
|
26
|
+
end
|
|
27
|
+
# rubocop:enable Metrics/ParameterLists
|
|
28
|
+
|
|
29
|
+
def deck?
|
|
30
|
+
@deck
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The ordered slide file paths that make up +content+: the explicit
|
|
34
|
+
# list in list order when +slides:+ names one, otherwise every
|
|
35
|
+
# discovered file in natural order.
|
|
36
|
+
def slide_paths
|
|
37
|
+
@slide_paths ||= deck? ? resolve_slide_paths : []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Every slide file the deck folder absorbs, so none is published on
|
|
41
|
+
# its own: every discovered direct child, plus any explicitly listed
|
|
42
|
+
# path (which may live in a subfolder). Order is not meaningful; this
|
|
43
|
+
# is a set for pruning, not the render order. Identical to
|
|
44
|
+
# +slide_paths+ when +slides:+ is +true+.
|
|
45
|
+
def consumed_paths
|
|
46
|
+
@consumed_paths ||= deck? ? resolve_consumed_paths : []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def content
|
|
50
|
+
parts = []
|
|
51
|
+
parts << @entry_body unless blank?(@entry_body)
|
|
52
|
+
parts.concat(slide_files.flat_map(&:slides))
|
|
53
|
+
parts.join("\n\n---\n\n")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def blank?(text)
|
|
59
|
+
text.to_s.strip.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def slide_files
|
|
63
|
+
@slide_files ||= slide_paths.map { |path| SlideFile.new(path, warning: @warning, read_options: @read_options) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def resolve_deck?
|
|
67
|
+
case @slides_option
|
|
68
|
+
when true, Array
|
|
69
|
+
true
|
|
70
|
+
when false, nil
|
|
71
|
+
false
|
|
72
|
+
when Hash
|
|
73
|
+
Support.warn(@warning, "Invalid slides value #{@slides_option.inspect}; set the top-level slides options " \
|
|
74
|
+
"(#{OPTION_KEYS.join(', ')}) directly instead of nesting them under slides:; " \
|
|
75
|
+
"treating as a single-file deck")
|
|
76
|
+
false
|
|
77
|
+
else
|
|
78
|
+
Support.warn(@warning, "Invalid slides value #{@slides_option.inspect}; expected true, false, or a list of " \
|
|
79
|
+
"file names; treating as a single-file deck")
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve_slide_paths
|
|
85
|
+
@slides_option == true ? discover_slide_paths : explicit_slide_paths
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve_consumed_paths
|
|
89
|
+
paths = discover_slide_paths
|
|
90
|
+
@slides_option.is_a?(Array) ? paths | explicit_slide_paths : paths
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def discover_slide_paths
|
|
94
|
+
paths = Dir.children(@directory).each_with_object([]) do |basename, found|
|
|
95
|
+
next if basename.start_with?("_", ".")
|
|
96
|
+
|
|
97
|
+
path = File.join(@directory, basename)
|
|
98
|
+
next unless File.file?(path)
|
|
99
|
+
next unless markdown_extension?(basename)
|
|
100
|
+
next if File.expand_path(path) == @entry_path
|
|
101
|
+
next unless published?(path)
|
|
102
|
+
|
|
103
|
+
found << path
|
|
104
|
+
end
|
|
105
|
+
paths.sort_by { |path| Support.natural_sort_key(File.basename(path)) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def markdown_extension?(basename)
|
|
109
|
+
@markdown_extensions.include?(File.extname(basename).delete_prefix(".").downcase)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Memoized because both the rendering set and the pruning set ask for
|
|
113
|
+
# it, and resolve warns about missing or out-of-bounds names.
|
|
114
|
+
def explicit_slide_paths
|
|
115
|
+
@explicit_slide_paths ||= @slides_option.filter_map { |name| resolve(name) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def resolve(name)
|
|
119
|
+
root = File.expand_path(@directory) + File::SEPARATOR
|
|
120
|
+
path = File.expand_path(File.join(@directory, name.to_s))
|
|
121
|
+
unless path.start_with?(root)
|
|
122
|
+
Support.warn(@warning, "Slide file #{name.inspect} escapes the deck directory; ignoring")
|
|
123
|
+
return nil
|
|
124
|
+
end
|
|
125
|
+
if path == @entry_path
|
|
126
|
+
Support.warn(@warning, "Slide file #{name.inspect} is the deck entry itself; ignoring")
|
|
127
|
+
return nil
|
|
128
|
+
end
|
|
129
|
+
unless File.file?(path)
|
|
130
|
+
Support.warn(@warning, "Slide file #{name.inspect} does not exist; ignoring")
|
|
131
|
+
return nil
|
|
132
|
+
end
|
|
133
|
+
unless published?(path)
|
|
134
|
+
Support.warn(@warning, "Slide file #{name.inspect} is excluded or not published; ignoring")
|
|
135
|
+
return nil
|
|
136
|
+
end
|
|
137
|
+
path
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Discovery reads the folder straight from disk, which bypasses the
|
|
141
|
+
# filtering Jekyll already applied: files its exclude: config drops and
|
|
142
|
+
# files whose front matter sets published: false are never read. Absorbing
|
|
143
|
+
# one would publish content the author withheld, inside the deck.
|
|
144
|
+
def published?(path)
|
|
145
|
+
return true unless @published
|
|
146
|
+
|
|
147
|
+
@published.call(File.expand_path(path))
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Jekyll
|
|
7
|
+
module Slides
|
|
8
|
+
# Finds folder-deck entries across a site's pages and output-collection
|
|
9
|
+
# documents, assembles each into one Markdown string via +Deck+, and
|
|
10
|
+
# prunes the slide files it consumed so they never appear in the built
|
|
11
|
+
# site. Runs once per site, inside the +:site, :post_read+ hook, before
|
|
12
|
+
# Liquid rendering and before +JekyllIntegration.process+ computes
|
|
13
|
+
# +slides_count+.
|
|
14
|
+
module DeckAssembler
|
|
15
|
+
class << self
|
|
16
|
+
# Assembles every folder deck the site's pages and output-collection
|
|
17
|
+
# documents opt into. Mutates +site.pages+, each collection's
|
|
18
|
+
# +docs+, and +site.static_files+ in place to remove consumed slide
|
|
19
|
+
# files.
|
|
20
|
+
def assemble(site)
|
|
21
|
+
published = published_paths(site)
|
|
22
|
+
entry_candidates(site).each { |entry| assemble_entry(site, entry, published) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Every source path Jekyll actually read. A file its exclude: config
|
|
28
|
+
# drops, or one whose front matter sets published: false, is absent
|
|
29
|
+
# here, and a deck must not absorb it. Snapshotted before any pruning,
|
|
30
|
+
# since pruning removes the very items this is built from.
|
|
31
|
+
def published_paths(site)
|
|
32
|
+
items = site.pages + site.collections.values.flat_map(&:docs) + site.static_files
|
|
33
|
+
items.to_set { |item| absolute_path(site, item) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def entry_candidates(site)
|
|
37
|
+
collection_documents = site.collections.values.flat_map(&:docs)
|
|
38
|
+
site.pages + collection_documents
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def assemble_entry(site, entry, published)
|
|
42
|
+
return unless presentation_entry?(entry)
|
|
43
|
+
|
|
44
|
+
slides_option = entry.data["slides"]
|
|
45
|
+
return unless slides_option
|
|
46
|
+
|
|
47
|
+
entry_path = absolute_path(site, entry)
|
|
48
|
+
unless index_entry?(entry_path, markdown_extensions(site))
|
|
49
|
+
warn_entry(site, entry_path, "the entry of a deck folder must be named index.<ext>; leaving as a single-file deck")
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
finish_assembly(site, entry, entry_path, build_deck(site, entry, entry_path, published))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def finish_assembly(site, entry, entry_path, deck)
|
|
57
|
+
return unless deck.deck?
|
|
58
|
+
|
|
59
|
+
entry.content = deck.content
|
|
60
|
+
# Pruning does not depend on anything being selected. Opting in
|
|
61
|
+
# absorbs the whole folder, so a file left out of an explicit
|
|
62
|
+
# running order is cut from the deck rather than published alone.
|
|
63
|
+
prune(site, deck.consumed_paths)
|
|
64
|
+
register_dependencies(site, entry_path, deck.slide_paths)
|
|
65
|
+
log_deck(site, entry_path, deck.slide_paths) unless deck.slide_paths.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def presentation_entry?(entry)
|
|
69
|
+
entry.respond_to?(:data) && entry.data.is_a?(Hash) && entry.data["layout"].to_s == "presentation"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def markdown_extensions(site)
|
|
73
|
+
site.config["markdown_ext"].to_s.split(",")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def index_entry?(path, extensions)
|
|
77
|
+
basename = File.basename(path)
|
|
78
|
+
extname = File.extname(basename)
|
|
79
|
+
return false if extname.empty?
|
|
80
|
+
|
|
81
|
+
base = basename.delete_suffix(extname)
|
|
82
|
+
ext = extname.delete_prefix(".").downcase
|
|
83
|
+
base.casecmp("index").zero? && extensions.any? { |candidate| candidate.to_s.downcase == ext }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_deck(site, entry, entry_path, published)
|
|
87
|
+
Deck.new(
|
|
88
|
+
directory: File.dirname(entry_path),
|
|
89
|
+
entry_path: entry_path,
|
|
90
|
+
entry_body: entry.content,
|
|
91
|
+
slides: entry.data["slides"],
|
|
92
|
+
markdown_extensions: markdown_extensions(site),
|
|
93
|
+
warning: ->(message) { warn_entry(site, entry_path, message) },
|
|
94
|
+
read_options: Jekyll::Utils.merged_file_read_opts(site, {}),
|
|
95
|
+
published: ->(path) { published.include?(path) }
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Removes every consumed slide file from site.pages, each
|
|
100
|
+
# collection's docs, and site.static_files, matching on absolute
|
|
101
|
+
# source path. A Markdown file with no front matter is read by
|
|
102
|
+
# Jekyll as a StaticFile rather than a Page, so pruning static
|
|
103
|
+
# files is required, not optional.
|
|
104
|
+
def prune(site, paths)
|
|
105
|
+
return if paths.empty?
|
|
106
|
+
|
|
107
|
+
consumed = paths.map { |path| File.expand_path(path) }
|
|
108
|
+
site.pages.reject! { |page| consumed.include?(absolute_path(site, page)) }
|
|
109
|
+
site.collections.each_value do |collection|
|
|
110
|
+
collection.docs.reject! { |doc| consumed.include?(absolute_path(site, doc)) }
|
|
111
|
+
end
|
|
112
|
+
site.static_files.reject! { |file| consumed.include?(absolute_path(site, file)) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def register_dependencies(site, entry_path, slide_paths)
|
|
116
|
+
return unless site.respond_to?(:regenerator) && site.regenerator.respond_to?(:add_dependency)
|
|
117
|
+
|
|
118
|
+
slide_paths.each { |path| site.regenerator.add_dependency(entry_path, path) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def log_deck(site, entry_path, slide_paths)
|
|
122
|
+
basenames = slide_paths.map { |path| File.basename(path) }
|
|
123
|
+
Jekyll.logger.info(
|
|
124
|
+
"Jekyll Slides:",
|
|
125
|
+
"#{relative_path(site, entry_path)}: #{slide_paths.length} slide files (#{basenames.join(', ')})"
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def warn_entry(site, entry_path, message)
|
|
130
|
+
Jekyll.logger.warn("Jekyll Slides:", "#{relative_path(site, entry_path)}: #{message}")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Jekyll::Page#path is relative to the source; Jekyll::Document#path
|
|
134
|
+
# and Jekyll::StaticFile#path are already absolute. expand_path
|
|
135
|
+
# handles all three, since it ignores the base for a path that is
|
|
136
|
+
# already absolute.
|
|
137
|
+
def absolute_path(site, item)
|
|
138
|
+
File.expand_path(item.path.to_s, site.source)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def relative_path(site, path)
|
|
142
|
+
Pathname.new(path).relative_path_from(Pathname.new(site.source)).to_s
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module Jekyll
|
|
7
|
+
module Slides
|
|
8
|
+
# Splits a leading YAML front matter block off a Markdown source string.
|
|
9
|
+
module FrontMatter
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Returns [values, body]. +values+ is a Hash for a well-formed leading
|
|
13
|
+
# +---+ block; otherwise +values+ is {} and +body+ is +source+ unchanged.
|
|
14
|
+
def split(source)
|
|
15
|
+
source = source.to_s
|
|
16
|
+
return [{}, source] unless source.start_with?("---\n", "---\r\n")
|
|
17
|
+
|
|
18
|
+
lines = source.lines
|
|
19
|
+
closing = lines[1..]&.index { |line| line.chomp == "---" }
|
|
20
|
+
return [{}, source] unless closing
|
|
21
|
+
|
|
22
|
+
closing += 1
|
|
23
|
+
values = parse(lines[1...closing].join)
|
|
24
|
+
return [{}, source] unless values
|
|
25
|
+
|
|
26
|
+
[values, lines[(closing + 1)..]&.join.to_s]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parse(text)
|
|
30
|
+
return {} if text.strip.empty?
|
|
31
|
+
|
|
32
|
+
parsed = YAML.safe_load(text, permitted_classes: [Date, Time, DateTime])
|
|
33
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
34
|
+
rescue Psych::Exception
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -6,14 +6,13 @@ module Jekyll
|
|
|
6
6
|
module Slides
|
|
7
7
|
module JekyllIntegration
|
|
8
8
|
DEFAULTS = {
|
|
9
|
-
"theme" =>
|
|
9
|
+
"theme" => DEFAULT_THEME,
|
|
10
10
|
"aspect_ratio" => "16:9",
|
|
11
11
|
"progress" => true,
|
|
12
12
|
"slide_numbers" => true,
|
|
13
13
|
"overview" => true,
|
|
14
14
|
"fullscreen" => true
|
|
15
15
|
}.freeze
|
|
16
|
-
THEMES = %w[minimal-light minimal-dark midnight ruby].freeze
|
|
17
16
|
BOOLEAN_OPTIONS = %w[progress slide_numbers overview fullscreen].freeze
|
|
18
17
|
OPTION_KEYS = DEFAULTS.keys.freeze
|
|
19
18
|
|
|
@@ -29,6 +28,7 @@ module Jekyll
|
|
|
29
28
|
|
|
30
29
|
def process_site(site)
|
|
31
30
|
install_resources(site)
|
|
31
|
+
DeckAssembler.assemble(site)
|
|
32
32
|
presentation_documents(site).each { |document| process(document) }
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -84,7 +84,7 @@ module Jekyll
|
|
|
84
84
|
theme = value.to_s
|
|
85
85
|
return theme if THEMES.include?(theme)
|
|
86
86
|
|
|
87
|
-
warning("Invalid theme #{value.inspect}; falling back to
|
|
87
|
+
warning("Invalid theme #{value.inspect}; expected one of #{THEMES.join(', ')}; falling back to #{DEFAULT_THEME}")
|
|
88
88
|
DEFAULTS[key]
|
|
89
89
|
when "aspect_ratio"
|
|
90
90
|
return value if value == DEFAULTS[key]
|
|
@@ -4,6 +4,20 @@ module Jekyll
|
|
|
4
4
|
module Slides
|
|
5
5
|
class Renderer < Jekyll::Renderer
|
|
6
6
|
def convert(content)
|
|
7
|
+
# Liquid has already run by the time this fires (Jekyll::Renderer#run
|
|
8
|
+
# converts before place_in_layouts), so this is the true slide
|
|
9
|
+
# count, including slides a {% for %} loop generated.
|
|
10
|
+
#
|
|
11
|
+
# document.data alone is not enough: a Document reaches the layout
|
|
12
|
+
# through a live DocumentDrop that reads document.data on every
|
|
13
|
+
# access, so writing it here is enough. A Page reaches the layout
|
|
14
|
+
# through a plain Hash snapshot that Jekyll::Renderer#assign_pages!
|
|
15
|
+
# already built and stashed in payload["page"] before convert runs,
|
|
16
|
+
# so a Page's layout would keep seeing the old, pre-Liquid count
|
|
17
|
+
# unless payload["page"] itself is updated too.
|
|
18
|
+
count = PresentationParser.new(content).parse.length
|
|
19
|
+
document.data["slides_count"] = count
|
|
20
|
+
payload["page"]["slides_count"] = count
|
|
7
21
|
Presentation.new(
|
|
8
22
|
content,
|
|
9
23
|
warning: method(:warning),
|