hadar 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0bc8dd031f42ad45785dbcf360c9f075e8d71758be0a5e2df0e158abc77ef8d
4
- data.tar.gz: 0702a59cfdcfd8987311f08e178a9390ec31b3442de35b4414fb1b3dc5c301d3
3
+ metadata.gz: ca92c68fe912ebaddf03792345b9747c02f994a1134f47c4481313f8312d0fe2
4
+ data.tar.gz: 5e1e1e5dd08e6516af6a540dd86c262a6da4ba175e532301a4e3c7fd5d9ebc7a
5
5
  SHA512:
6
- metadata.gz: a234432c82f1b580bc19b3f4c29211a98556f277f28f5cdbd9da7c1f08172b9fcddea6f64c9a9dba1bcbe3204e071391f20ad6c48a8aa87ec04f19643f59fc83
7
- data.tar.gz: ca5aadb8917b2aa2761592ecec9a7adc98e0d22b24fe1e30f20d398afac625c6ba5b10e5c60f13b84ceeff3e2a44769e732eea2fa55d8f060705ceb90bc0c0c0
6
+ metadata.gz: aa6fe7bc92f62e5d4780e6e2b6236a82949f6bab93858243d734a4953146028028ff8b45345829c774b0d35277f9bb459b005178133768559ab473c22d6ddcab
7
+ data.tar.gz: 62d2be12ec8b34ab18bcc71866edd8a4ce8e40d0fc67f3cb5d853dab520a5e4f8fd28cda7e8ea4648946f471f9b410aec0c1e07d85cb6dff9ac6cc8c368fded8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-09-24
4
+
5
+ - Use the preview's Zaniah element tree for searchable vector PDF export, including freeform slides.
6
+
3
7
  ## 0.3.0 - 2026-09-24
4
8
 
5
9
  - Add opt-in freeform slide placement with explicit Markdown directives and an editor warning.
data/README.md CHANGED
@@ -1,38 +1,58 @@
1
- # Hadar
2
-
3
- Hadar (β Centauri) is a Markdown-backed presentation app. Its deck model is a
4
- projection of Beid's source-positioned AST: slide content and slots keep their
5
- source nodes instead of becoming a second mutable copy. Markdown `---`
6
- thematic breaks separate slides, and `<!-- layout: ... -->` selects one of
7
- eight template layouts.
8
-
9
- Hadar provides deck/slide/slot parsing, three JSONC themes, layout selection, a
10
- declarative preview tree, and virtualized thumbnail rows built with Zaniah's
11
- existing `Describe` and `UniformList` APIs. Its selected-slide editor can switch
12
- among every declared layout slot. Text slots use Zaniah `RichText` and Beid;
13
- image slots provide insert/replace actions, while tables and fenced code expose
14
- source-preserving cell/body editors. Unsupported syntax remains visible but
15
- read-only. Beid-backed
16
- `<!-- notes: ... -->` comments provide speaker notes on each slide and are
17
- excluded from the visible preview. Image slots can insert and replace
18
- source-backed Markdown references; absolute asset paths are stored relative to
19
- the opened deck. Local image slots render through Zaniah's image decoder (PNG,
20
- GIF, and baseline JPEG); relative references resolve from the deck's directory.
21
- Missing, unreadable, and unsupported images fail preview construction with a
22
- `Hadar::Error`. Remote URLs are not fetched. Hadar does not copy image files;
23
- saving continues to write only the Markdown source. Its window host polls for
24
- external Markdown edits and can show a next-slide, notes, and elapsed-time
25
- presenter view. The host supports slide navigation, fullscreen, a fuzzy command
26
- palette, PNG-sequence, and APNG export. When a secondary display is available,
27
- the owned presenter window opens there fullscreen.
1
+ <h1 align="center">Hadar</h1>
2
+
3
+ <p align="center">
4
+ <strong>Markdown-backed slide decks with source-preserving editing, live preview, and presentation export</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://rubygems.org/gems/hadar"><img src="https://img.shields.io/gem/v/hadar.svg" alt="Gem version"></a>
9
+ <a href="https://rubygems.org/gems/hadar"><img src="https://img.shields.io/gem/dt/hadar.svg" alt="Gem downloads"></a>
10
+ <a href="https://github.com/noxdea/hadar/actions/workflows/main.yml"><img src="https://github.com/noxdea/hadar/actions/workflows/main.yml/badge.svg" alt="CI"></a>
11
+ <img src="https://img.shields.io/badge/CRuby-%3E%3D%203.2-cc342d.svg" alt="CRuby 3.2 or newer">
12
+ <a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license"></a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="#features">Features</a> ·
17
+ <a href="#installation">Installation</a> ·
18
+ <a href="#quick-start">Quick start</a> ·
19
+ <a href="#editing-and-presenting">Editing and presenting</a> ·
20
+ <a href="#export">Export</a>
21
+ </p>
22
+
23
+ ---
24
+
25
+ Hadar is a Ruby presentation library built around ordinary Markdown files.
26
+ [Beid](https://github.com/noxdea/beid) keeps the source-positioned document as
27
+ the only editable copy; Hadar projects it into slides, slots, and a live
28
+ [Zaniah](https://github.com/noxdea/zaniah) preview. Use its API to embed a
29
+ presentation window in an app or export a deck without a GUI.
30
+
31
+ ## Features
32
+
33
+ - Eight automatic or explicitly selected slide templates, plus opt-in freeform placement
34
+ - Source-backed text, image, table-cell, and fenced-code editing with speaker notes
35
+ - Three built-in JSONC themes (`minimal`, `dark`, `warm`) and custom themes
36
+ - Virtualized slide thumbnails, keyboard navigation, command palette, and a secondary-display presenter view
37
+ - Searchable PDF, PNG-sequence, and animated PNG (APNG) export
28
38
 
29
39
  ## Installation
30
40
 
31
- ```ruby
32
- gem "hadar"
41
+ Hadar requires CRuby 3.2 or newer. Install the gem or add `gem "hadar"` to
42
+ your Gemfile:
43
+
44
+ ```sh
45
+ gem install hadar
33
46
  ```
34
47
 
35
- ## Usage
48
+ Hadar is a library; it does not install a `hadar` command. Windowed use
49
+ requires a Zaniah-supported display backend. PNG and APNG export use Zaniah's
50
+ headless renderer.
51
+
52
+ ## Quick start
53
+
54
+ Separate slides with Markdown thematic breaks. Hadar selects a layout from
55
+ each slide's content unless you add a `layout` comment:
36
56
 
37
57
  ```ruby
38
58
  require "hadar"
@@ -59,21 +79,16 @@ deck = Hadar::Deck.parse(<<~MARKDOWN)
59
79
  :::
60
80
  MARKDOWN
61
81
 
62
- deck.slide(1).layout # => :two_column
63
- deck.slide(1).slot(:left).text
64
- deck.slide(0).notes # => nil when no speaker notes are present
65
-
82
+ deck.slide(1).layout # => :two_column
83
+ deck.slide(1).slot(:left).text # => "Revenue rose 18% year over year."
66
84
  tree = Hadar::Renderer.new.describe(deck.slide(0))
67
- element = Hadar::Renderer.new.build(deck.slide(0))
68
-
69
- thumbnails = Hadar::SlideList.new(deck, selected: 0,
70
- on_select: ->(slide, index) { puts "Selected slide #{index + 1}: #{slide.title}" })
71
- list_element = thumbnails.build(width: 280, height: 640)
72
85
  ```
73
86
 
74
- Built-in themes are `minimal`, `dark`, and `warm`. Custom JSONC themes can be
75
- loaded with `Hadar::Theme.load(path)` and passed to `Deck.parse` or
76
- `Deck.open`.
87
+ `Renderer#build` returns a Zaniah element for embedding in a preview.
88
+
89
+ ## Editing and presenting
90
+
91
+ ### Source-backed slots
77
92
 
78
93
  Slots remain projections of the current Beid document. `slot.rich_text` returns
79
94
  a Zaniah rich-text editor whose bold, italic, link, and code spans come from the
@@ -89,6 +104,7 @@ paragraph styles are rejected rather than flattening or normalizing markup.
89
104
  Rich-text projection covers headings, paragraphs, block quotes, and text lists;
90
105
  tables, fenced code blocks, and Markdown strikethrough are not editable through
91
106
  this API yet.
107
+
92
108
  Slots returned before a successful edit are stale snapshots. Opened decks save
93
109
  atomically, preserve file permissions, and refuse to overwrite external changes:
94
110
 
@@ -101,22 +117,22 @@ deck.save
101
117
  ```
102
118
 
103
119
  Opened decks can detect and reload external changes directly or through the
104
- Zaniah platform watcher. A watcher is polled by the host's UI loop; successful
105
- reloads update the same deck object and invoke `on_reload`. Reload refuses to
106
- discard local unsaved Markdown edits, leaving both the in-memory and on-disk
107
- versions untouched. The host can keep its current slide selection and rebuild
108
- the preview after the callback:
120
+ Zaniah platform watcher. Reload refuses to discard unsaved local edits. A
121
+ watcher is polled by the host's UI loop; successful reloads update the same
122
+ deck object and invoke `on_reload`:
109
123
 
110
124
  ```ruby
111
125
  watcher = deck.watch(on_reload: ->(_deck) { window.request_frame })
112
126
  watcher.poll(timeout: 0)
113
127
  ```
114
128
 
115
- `deck.reload_if_changed` performs the same safe check without a platform watcher.
116
- `Hadar::Application` wires this polling into attached windows and retains the
117
- selected slide by index when a reload changes the deck. Its presenter view shows
118
- the next slide, current slide's notes, and elapsed time. While presenting, each
119
- window tick requests a fresh frame so elapsed time stays current:
129
+ `deck.reload_if_changed` performs the same safe check without a watcher.
130
+
131
+ ### Application and controls
132
+
133
+ `Hadar::Application` wires the watcher into attached windows and keeps the
134
+ selected slide index on reload. Its presenter view shows the next slide,
135
+ current notes, and elapsed time:
120
136
 
121
137
  ```ruby
122
138
  app = Hadar::Application.new(deck)
@@ -125,47 +141,17 @@ app.attach(main_window: main)
125
141
  app.run
126
142
  ```
127
143
 
128
- `Application#run` polls the deck and ticks all attached windows in one loop.
129
- When at least two displays are available, Hadar creates its own presenter
130
- window and places it fullscreen on a secondary display (Wayland compositors may
131
- choose whether to honor the output request). With one display, it does not
132
- create a presenter automatically. A presenter window passed by the host is
133
- used as-is and is never moved or closed by Hadar. The automatically created
134
- presenter closes when the main window closes. Rich-text slot editors are backed
135
- by the current Markdown source and recreated after an external reload.
136
- They keep the caret or selection (and editor focus) when selected text maps
137
- unambiguously around one contiguous external edit; if an edit overlaps the
138
- selection or makes the mapping ambiguous, the selection and focus are cleared
139
- rather than moved to unrelated text. Arrow, Page Up/Down, Home, and End navigate
140
- slides; `P` or `F5` starts presentation,
141
- `F11` toggles fullscreen, and `Escape` exits presentation or fullscreen.
142
- `Ctrl/Cmd-K` opens the fuzzy command palette; `Ctrl/Cmd-S` saves an opened deck
143
- through its conflict-aware atomic writer.
144
-
145
- PDF export produces one searchable 16:9 page for every slide, including all
146
- eight layouts. Pass a TrueType font that contains every visible character; the
147
- default font is selected from Zaniah's local font database:
148
-
149
- ```ruby
150
- Hadar::Export::PDF.write(deck, "slides.pdf", font: "/path/to/font.ttf")
151
- ```
152
-
153
- Okab currently requires TrueType outlines for PDF embedding. Hadar exports local
154
- PNG and JPEG images; remote images and other image formats are rejected.
155
-
156
- PNG sequence export renders all slides at the requested dimensions using
157
- Zaniah's headless renderer. It refuses to overwrite existing frames:
144
+ `Application#run` polls and ticks its windows. With a second display, Hadar
145
+ opens its own fullscreen presenter window there; an explicitly passed
146
+ `presenter_window:` stays under the host's control. After an external reload,
147
+ rich-text editors are recreated. Their selection and focus survive only when
148
+ they map unambiguously around the edit.
158
149
 
159
- ```ruby
160
- app.export_png_sequence("slides-png", width: 1280, height: 720)
161
- ```
162
-
163
- Animated PNG export holds each slide for three seconds by default, loops forever,
164
- and refuses to replace an existing target unless `overwrite: true` is passed:
150
+ Arrow keys, Page Up/Down, Home, and End navigate slides. `P` or `F5` starts
151
+ presentation; `F11` toggles fullscreen; `Escape` exits either mode.
152
+ `Ctrl/Cmd-K` opens the command palette, and `Ctrl/Cmd-S` saves an opened deck.
165
153
 
166
- ```ruby
167
- app.export_apng("slides.apng", width: 1280, height: 720, duration_ms: 2500)
168
- ```
154
+ ### Notes and block editors
169
155
 
170
156
  Speaker notes can be written as a one-line or multiline HTML comment. Their
171
157
  Markdown remains in the source unchanged and does not appear in slide slots or
@@ -206,7 +192,7 @@ For a deck created with `Deck.parse`, pass a new path to `app.save(path)`.
206
192
  Replacing an existing unrelated path requires `overwrite: true`. Opened decks
207
193
  can use `app.save` or `Ctrl/Cmd-S`; the same external-change check applies.
208
194
 
209
- ## Layouts
195
+ ### Layouts and themes
210
196
 
211
197
  `title`, `title+body`, `two-column`, `image+text`, `full-bleed-image`,
212
198
  `quote`, `code`, and `blank` are available. Explicit layout directives take
@@ -222,6 +208,11 @@ the referenced assets are not copied. If an absolute selected asset lives
222
208
  outside the deck directory, its saved relative reference points outside that
223
209
  directory rather than copying the file.
224
210
 
211
+ Built-in themes are `minimal`, `dark`, and `warm`. Custom JSONC themes can be
212
+ loaded with `Hadar::Theme.load(path)` and passed to `Deck.parse` or
213
+ `Deck.open`. A deck can also set a built-in theme in YAML front matter with
214
+ `theme: dark`.
215
+
225
216
  Freeform placement is an explicit per-slide opt-in. Add `<!-- layout: freeform -->`
226
217
  and one `<!-- place: x,y,width,height -->` immediately before each Markdown
227
218
  block. Coordinates are percentages of the slide's inner canvas (after theme
@@ -246,19 +237,47 @@ After an external edit reloads a freeform slide, select its block again before
246
237
  editing; source-order item numbers may have changed.
247
238
 
248
239
  `SlideList` creates thumbnail rows only for the visible viewport, using
249
- `Zaniah::UniformList`; `build(width:, height:)` returns the Zaniah element for
250
- embedding in an application layout. Its `select(index)` method updates the
251
- selection and invokes the optional `on_select` callback. Wezen is not involved
252
- in live thumbnails; it encodes rendered slide frames as APNG.
240
+ `Zaniah::UniformList`. `build(width:, height:)` returns a Zaniah element;
241
+ `select(index)` updates the selection and invokes `on_select` when supplied:
242
+
243
+ ```ruby
244
+ thumbnails = Hadar::SlideList.new(deck, selected: 0,
245
+ on_select: ->(slide, index) { puts "Selected slide #{index + 1}: #{slide.title}" })
246
+ element = thumbnails.build(width: 280, height: 640)
247
+ ```
248
+
249
+ ## Export
250
+
251
+ ```ruby
252
+ Hadar::Export::PDF.write(deck, "slides.pdf", font: "/path/to/font.ttf")
253
+ Hadar::Export::PNGSequence.write(deck, "slides-png", width: 1280, height: 720)
254
+ Hadar::Export::APNG.write(deck, "slides.apng", width: 1280, height: 720,
255
+ duration_ms: 2500)
256
+ ```
257
+
258
+ PDF creates one searchable 16:9 page per slide from the same element tree used
259
+ by PNG export, including freeform placement.
260
+ It needs a TrueType-outline font containing every visible character; without
261
+ `font:`, Hadar uses Zaniah's local font database. PDF embeds local PNG and JPEG
262
+ images, but rejects remote and other image formats. PNG-sequence export refuses
263
+ to replace existing frames. APNG defaults to three seconds per slide and
264
+ infinite looping; an existing target requires `overwrite: true`.
265
+ `Application#export_png_sequence` and
266
+ `#export_apng` wrap the corresponding exporters.
253
267
 
254
268
  ## Development
255
269
 
256
- Run the specs with `bundle exec rake`. Check the 100-slide virtual-list
257
- layout/scene-build budget with `BUDGET=1 bundle exec ruby bench/slide_list.rb`;
258
- the headless benchmark skips software pixel rasterization. Hadar depends on
259
- Beid, Antares, Kochab, Okab, Spica, Wezen, Xamidimura, and Zaniah; its
260
- declarative `Describe`, `UniformList`, and input keymap APIs are reused directly.
270
+ ```sh
271
+ bundle install
272
+ bundle exec rake
273
+ ```
274
+
275
+ Check the 100-slide thumbnail layout/scene-build budget with
276
+ `BUDGET=1 bundle exec ruby bench/slide_list.rb`. See the
277
+ [template-layout](docs/adr/001-template-layouts.md) and
278
+ [freeform-placement](docs/adr/002-opt-in-freeform-layout.md) decisions for the
279
+ source model.
261
280
 
262
281
  ## License
263
282
 
264
- MIT. See [LICENSE.txt](LICENSE.txt).
283
+ Hadar is released under the [MIT License](LICENSE.txt).
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "okab/zaniah_vector"
4
+ require "zaniah/vector"
5
+
3
6
  module Hadar
4
7
  module Export
5
8
  class PDF
@@ -23,10 +26,12 @@ module Hadar
23
26
 
24
27
  def render
25
28
  document = Okab::Document.new(title: @deck.slides.first&.title, creator: "Hadar")
29
+ renderer = Renderer.new(font: @font.face)
26
30
  @deck.slides.each do |slide|
31
+ ensure_glyphs!(renderer.describe(slide))
32
+ vector = Zaniah::Vector.record(width: WIDTH, height: HEIGHT) { renderer.build(slide) }
27
33
  page = document.page(width: WIDTH, height: HEIGHT)
28
- paint_background(page, slide.theme)
29
- paint_slide(page, slide)
34
+ Okab::ZaniahVector.draw(page, vector)
30
35
  document.outline(slide.title, page: page) unless slide.title.empty?
31
36
  end
32
37
  document.render
@@ -52,94 +57,16 @@ module Hadar
52
57
  raise Error, "cannot load PDF font: #{error.message}"
53
58
  end
54
59
 
55
- def paint_background(page, theme)
56
- page.rect(0, 0, WIDTH, HEIGHT).fill(rgb(theme.colors.fetch("background")))
57
- end
58
-
59
- def paint_slide(page, slide)
60
- margin = slide.theme.spacing.fetch("margin")
61
- gap = slide.theme.spacing.fetch("gap")
62
- title_size = slide.theme.font.fetch("title_size")
63
- body_size = slide.theme.font.fetch("body_size")
64
- text_color = rgb(slide.theme.colors.fetch("text"))
65
- muted = rgb(slide.theme.colors.fetch("muted"))
66
- width = WIDTH - margin * 2
60
+ def ensure_glyphs!(node)
61
+ if %i[text code_token].include?(node.type)
62
+ missing = node.props.fetch(:text).codepoints.find do |codepoint|
63
+ next false if codepoint == 0x0a || variation_selector?(codepoint)
67
64
 
68
- case slide.layout
69
- when :title
70
- text(page, slide.slot(:title).text, margin, HEIGHT * 0.57, width, title_size, text_color, :center)
71
- text(page, slide.slot(:subtitle).text, margin, HEIGHT * 0.40, width, body_size, muted, :center)
72
- when :two_column
73
- paint_title(page, slide, margin, width, title_size, text_color)
74
- column_width = (width - gap) / 2
75
- y = HEIGHT - margin - title_size - gap - body_size
76
- text(page, slide.slot(:left).text, margin, y, column_width, body_size, text_color)
77
- text(page, slide.slot(:right).text, margin + column_width + gap, y, column_width, body_size, text_color)
78
- when :image_text
79
- paint_title(page, slide, margin, width, title_size, text_color)
80
- y = HEIGHT - margin - title_size - gap - body_size
81
- text(page, slide.slot(:text).text, margin, y, width * 0.48, body_size, text_color)
82
- paint_image(page, slide.slot(:image), margin + width * 0.52, margin,
83
- width * 0.48, HEIGHT - margin * 2 - title_size - gap)
84
- when :full_bleed_image
85
- paint_image(page, slide.slot(:image), 0, 0, WIDTH, HEIGHT, cover: true)
86
- when :quote
87
- text(page, slide.slot(:quote).text, margin + width * 0.08, HEIGHT * 0.62,
88
- width * 0.84, title_size, text_color, :center)
89
- text(page, slide.slot(:attribution).text, margin, HEIGHT * 0.28, width, body_size, muted, :center)
90
- when :code
91
- paint_title(page, slide, margin, width, title_size, text_color)
92
- y = HEIGHT - margin - title_size - gap - body_size
93
- text(page, slide.slot(:code).text, margin, y, width, body_size, text_color)
94
- when :blank
95
- nil
96
- else
97
- paint_title(page, slide, margin, width, title_size, text_color)
98
- y = HEIGHT - margin - title_size - gap - body_size
99
- text(page, slide.slot(:body).text, margin, y, width, body_size, text_color)
65
+ @font.face.glyph_id(codepoint).zero?
66
+ end
67
+ raise Error, "PDF font does not contain U+#{missing.to_s(16).upcase}; provide a font that covers the deck text" if missing
100
68
  end
101
- end
102
-
103
- def paint_title(page, slide, margin, width, size, color)
104
- text(page, slide.slot(:title).text, margin, HEIGHT - margin - size, width, size, color)
105
- end
106
-
107
- def text(page, value, x, y, width, size, color, align = :left)
108
- return if value.nil? || value.empty?
109
-
110
- value = value.gsub(/\r\n?/, "\n")
111
- ensure_glyphs!(value)
112
- page.text_block(value, x: x, y: y, width: width, font: @font, size: size,
113
- line_height: size * 1.3, align: align, color: color)
114
- end
115
-
116
- def paint_image(page, slot, x, y, width, height, cover: false)
117
- return if slot.empty?
118
-
119
- image = Okab::Image.decode(File.binread(slot.resolved_image_path))
120
- scales = [width / image.width, height / image.height]
121
- scale = cover ? scales.max : scales.min
122
- draw_width, draw_height = image.width * scale, image.height * scale
123
- page.clip { |path| path.rect(x, y, width, height) } if cover
124
- page.image(image, x: x + (width - draw_width) / 2, y: y + (height - draw_height) / 2,
125
- width: draw_width, height: draw_height)
126
- rescue Okab::Error, SystemCallError => error
127
- raise Error, "cannot export image: #{error.message}"
128
- end
129
-
130
- def ensure_glyphs!(value)
131
- missing = value.codepoints.find do |codepoint|
132
- next false if codepoint == 0x0a || variation_selector?(codepoint)
133
-
134
- @font.face.glyph_id(codepoint).zero?
135
- end
136
- return unless missing
137
-
138
- raise Error, "PDF font does not contain U+#{missing.to_s(16).upcase}; provide a font that covers the deck text"
139
- end
140
-
141
- def rgb(color)
142
- color.delete_prefix("#").scan(/../).first(3).map { |component| component.to_i(16) / 255.0 }
69
+ node.children.each { |child| ensure_glyphs!(child) }
143
70
  end
144
71
 
145
72
  def variation_selector?(codepoint)
@@ -2,9 +2,10 @@
2
2
 
3
3
  module Hadar
4
4
  class Renderer
5
- def initialize
5
+ def initialize(font: nil)
6
6
  @font_db = nil
7
7
  @fonts = {}
8
+ @font_override = font
8
9
  @vocabulary = build_vocabulary
9
10
  end
10
11
 
@@ -92,6 +93,8 @@ module Hadar
92
93
  end
93
94
 
94
95
  def font(family)
96
+ return @font_override if @font_override
97
+
95
98
  @fonts[family] ||= begin
96
99
  @font_db ||= Zaniah::TextSystem::FontDB.new
97
100
  @font_db.find(family: family == "sans-serif" ? nil : family)
data/lib/hadar/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hadar
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/sig/hadar.rbs CHANGED
@@ -184,7 +184,7 @@ module Hadar
184
184
  end
185
185
 
186
186
  class Renderer
187
- def initialize: () -> void
187
+ def initialize: (?font: Alhena::Font?) -> void
188
188
  def describe: (Slide slide) -> Zaniah::Describe::Node
189
189
  def build: (Slide slide) -> Zaniah::Element
190
190
  def surface: (Slide slide) -> Zaniah::Describe::Surface
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hadar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: 0.1.0
60
+ version: 0.2.0
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: 0.1.0
67
+ version: 0.2.0
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: spica
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -113,14 +113,14 @@ dependencies:
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: 0.6.0
116
+ version: 0.9.0
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: 0.6.0
123
+ version: 0.9.0
124
124
  description: Projects Beid Markdown ASTs into template-based slide decks, edits supported
125
125
  source-backed text, and exports PDF or PNG.
126
126
  email: