hadar 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +234 -0
- data/assets/themes/dark.jsonc +11 -0
- data/assets/themes/minimal.jsonc +12 -0
- data/assets/themes/warm.jsonc +11 -0
- data/docs/adr/001-template-layouts.md +22 -0
- data/lib/hadar/application.rb +749 -0
- data/lib/hadar/command_palette.rb +57 -0
- data/lib/hadar/deck.rb +440 -0
- data/lib/hadar/deck_watcher.rb +33 -0
- data/lib/hadar/export/pdf.rb +150 -0
- data/lib/hadar/export/png_sequence.rb +63 -0
- data/lib/hadar/layout.rb +80 -0
- data/lib/hadar/presenter.rb +92 -0
- data/lib/hadar/renderer.rb +238 -0
- data/lib/hadar/rich_text_projection.rb +110 -0
- data/lib/hadar/rich_text_writeback.rb +377 -0
- data/lib/hadar/slide.rb +130 -0
- data/lib/hadar/slide_list.rb +74 -0
- data/lib/hadar/slot.rb +149 -0
- data/lib/hadar/theme.rb +85 -0
- data/lib/hadar/version.rb +5 -0
- data/lib/hadar.rb +31 -0
- data/sig/hadar.rbs +221 -0
- metadata +168 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4faa68921b499534d3fd9985fa24680fa9a63bd95664bb808e7f1d072527ea75
|
|
4
|
+
data.tar.gz: e9c02868a90d8e5f479cef86f1b1a76d826c34509c71a2c6ef9c1dda76b10c2f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6f3a0b49bcd1a6a8d436421cc05965bcf6e6ca71910810d2e0c05a49b3e97949dc49248e7352b184b251ca3f5d654a1a844683791238e4c6684abb03abb9daeb
|
|
7
|
+
data.tar.gz: c79aaf224353506b80a6a7d0162fcc935fa9201d39a55e877495c1807b47980d2d9788ecff65ae04810563aa828f1a10c0cdca138e0b1fd01ad13bda5c0c3364
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
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, and PNG-sequence export. When a secondary display is available, the
|
|
27
|
+
owned presenter window opens there fullscreen.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem "hadar"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "hadar"
|
|
39
|
+
|
|
40
|
+
deck = Hadar::Deck.parse(<<~MARKDOWN)
|
|
41
|
+
---
|
|
42
|
+
theme: dark
|
|
43
|
+
---
|
|
44
|
+
# Quarterly report
|
|
45
|
+
|
|
46
|
+
## 2026 Q3
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
<!-- layout: two-column -->
|
|
51
|
+
# Revenue
|
|
52
|
+
|
|
53
|
+
::: left
|
|
54
|
+
Revenue rose 18% year over year.
|
|
55
|
+
:::
|
|
56
|
+
|
|
57
|
+
::: right
|
|
58
|
+
New customers: 42
|
|
59
|
+
:::
|
|
60
|
+
MARKDOWN
|
|
61
|
+
|
|
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
|
+
|
|
66
|
+
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
|
+
```
|
|
73
|
+
|
|
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`.
|
|
77
|
+
|
|
78
|
+
Slots remain projections of the current Beid document. `slot.rich_text` returns
|
|
79
|
+
a Zaniah rich-text editor whose bold, italic, link, and code spans come from the
|
|
80
|
+
Markdown source. Text edits are written back through Beid immediately when they
|
|
81
|
+
stay within one source-backed text run, preserving all other source bytes and
|
|
82
|
+
existing markers. Bold and italic may be added to one source-backed run; removal
|
|
83
|
+
is supported for a simple complete bold/italic text run. Existing bullet and
|
|
84
|
+
numbered list items can be indented or outdented one level at a time through
|
|
85
|
+
`RichText#paragraph_style(..., level:)`; edits preserve the original list marker
|
|
86
|
+
and reject changes that would reparent neighboring items. Edits crossing Markdown
|
|
87
|
+
structure, using non-Markdown styles (such as color or font size), or using other
|
|
88
|
+
paragraph styles are rejected rather than flattening or normalizing markup.
|
|
89
|
+
Rich-text projection covers headings, paragraphs, block quotes, and text lists;
|
|
90
|
+
tables, fenced code blocks, and Markdown strikethrough are not editable through
|
|
91
|
+
this API yet.
|
|
92
|
+
Slots returned before a successful edit are stale snapshots. Opened decks save
|
|
93
|
+
atomically, preserve file permissions, and refuse to overwrite external changes:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
deck = Hadar::Deck.open("slides.md")
|
|
97
|
+
deck.slide(0).slot(:title).replace_text("Revised title")
|
|
98
|
+
body = deck.slide(1).slot(:left).rich_text
|
|
99
|
+
body.replace(0..."Revenue".bytesize, "Turnover") # Beid updates only that source text run
|
|
100
|
+
deck.save
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
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:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
watcher = deck.watch(on_reload: ->(_deck) { window.request_frame })
|
|
112
|
+
watcher.poll(timeout: 0)
|
|
113
|
+
```
|
|
114
|
+
|
|
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:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
app = Hadar::Application.new(deck)
|
|
123
|
+
main = Zaniah::Platform.open_window(title: "Hadar")
|
|
124
|
+
app.attach(main_window: main)
|
|
125
|
+
app.run
|
|
126
|
+
```
|
|
127
|
+
|
|
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:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
app.export_png_sequence("slides-png", width: 1280, height: 720)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Speaker notes can be written as a one-line or multiline HTML comment. Their
|
|
164
|
+
Markdown remains in the source unchanged and does not appear in slide slots or
|
|
165
|
+
the preview tree:
|
|
166
|
+
|
|
167
|
+
```markdown
|
|
168
|
+
<!-- notes:
|
|
169
|
+
Explain the chart's assumptions.
|
|
170
|
+
|
|
171
|
+
Call out the remaining risk.
|
|
172
|
+
-->
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Read them with `deck.slide(0).notes`.
|
|
176
|
+
|
|
177
|
+
Tables in body slots render as rows and cells. `slot.table_rows` returns plain
|
|
178
|
+
cell text (including the header as row `0`), and `replace_table_cell(row:,
|
|
179
|
+
column:, value:, table: 0)` updates exactly one cell through Beid. Indices are
|
|
180
|
+
zero-based. Cell values containing Markdown delimiters, pipes, or newlines are
|
|
181
|
+
rejected because Beid cannot currently round-trip escaped table-cell syntax
|
|
182
|
+
safely. In the app, choose the table-containing slot, select a table/row/column,
|
|
183
|
+
edit the cell text, then use **Apply cell**; invalid edits leave the source
|
|
184
|
+
unchanged.
|
|
185
|
+
|
|
186
|
+
Fenced code blocks render in monospace with syntax colors from Antares/Rouge
|
|
187
|
+
when the fence info names a supported language. Unknown language names remain
|
|
188
|
+
plain monospace. A slide containing other content besides its title and fenced
|
|
189
|
+
blocks uses the body layout so its text and tables remain visible.
|
|
190
|
+
`slot.replace_code(text, block: 0)` updates only the body of a
|
|
191
|
+
closed fenced block and preserves its fence, info string, and surrounding
|
|
192
|
+
Markdown. The app's code pane selects among fenced blocks and applies the edited
|
|
193
|
+
body without replacing neighboring content. An omitted final newline is
|
|
194
|
+
restored using the existing line ending;
|
|
195
|
+
unclosed and indented code blocks are display-only. A replacement containing a
|
|
196
|
+
line that would close the fence is rejected.
|
|
197
|
+
|
|
198
|
+
For a deck created with `Deck.parse`, pass a new path to `app.save(path)`.
|
|
199
|
+
Replacing an existing unrelated path requires `overwrite: true`. Opened decks
|
|
200
|
+
can use `app.save` or `Ctrl/Cmd-S`; the same external-change check applies.
|
|
201
|
+
|
|
202
|
+
## Layouts
|
|
203
|
+
|
|
204
|
+
`title`, `title+body`, `two-column`, `image+text`, `full-bleed-image`,
|
|
205
|
+
`quote`, `code`, and `blank` are available. Explicit layout directives take
|
|
206
|
+
precedence; otherwise Hadar selects a layout from the Beid AST. Image slots
|
|
207
|
+
render local assets through `Zaniah::Image`; use `slot.resolved_image_path` to
|
|
208
|
+
resolve a source destination relative to its deck. An empty image slot can use
|
|
209
|
+
`insert_image(path, alt:)`; an existing single-image slot can use
|
|
210
|
+
`replace_image(path)`. The app exposes these actions from the image slot using
|
|
211
|
+
the platform file chooser. Relative paths are retained, while absolute paths to
|
|
212
|
+
existing files are made relative to the deck's directory. Insertion and
|
|
213
|
+
replacement update only the Markdown image destination or add one image node;
|
|
214
|
+
the referenced assets are not copied. If an absolute selected asset lives
|
|
215
|
+
outside the deck directory, its saved relative reference points outside that
|
|
216
|
+
directory rather than copying the file.
|
|
217
|
+
|
|
218
|
+
`SlideList` creates thumbnail rows only for the visible viewport, using
|
|
219
|
+
`Zaniah::UniformList`; `build(width:, height:)` returns the Zaniah element for
|
|
220
|
+
embedding in an application layout. Its `select(index)` method updates the
|
|
221
|
+
selection and invokes the optional `on_select` callback. Wezen is not involved
|
|
222
|
+
in live thumbnails; it encodes raster frames for export workflows.
|
|
223
|
+
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
Run the specs with `bundle exec rake`. Check the 100-slide virtual-list
|
|
227
|
+
layout/scene-build budget with `BUDGET=1 bundle exec ruby bench/slide_list.rb`;
|
|
228
|
+
the headless benchmark skips software pixel rasterization. Hadar depends on
|
|
229
|
+
Beid, Antares, Kochab, Okab, Spica, Xamidimura, and Zaniah; its declarative `Describe`,
|
|
230
|
+
`UniformList`, and input keymap APIs are reused directly.
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
// A bright, high-contrast deck theme.
|
|
3
|
+
"name": "Minimal",
|
|
4
|
+
"colors": {
|
|
5
|
+
"background": "#ffffff",
|
|
6
|
+
"text": "#18212b",
|
|
7
|
+
"muted": "#5c6875",
|
|
8
|
+
"accent": "#315b7c"
|
|
9
|
+
},
|
|
10
|
+
"font": { "family": "sans-serif", "title_size": 44, "body_size": 26 },
|
|
11
|
+
"spacing": { "margin": 64, "gap": 24 }
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR 001: Template layouts over a free-form canvas
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-23
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Markdown is linear and structural, while a free-form slide canvas depends on
|
|
9
|
+
coordinates. Silently adding canvas positions would make GUI edits impossible
|
|
10
|
+
to preserve in the Markdown source.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
The first version uses eight named layouts with semantic slots. An optional
|
|
15
|
+
`<!-- layout: ... -->` directive selects a template; otherwise Hadar infers
|
|
16
|
+
one from the parsed Markdown structure. A free-form canvas is out of scope.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
|
|
20
|
+
Template content can remain a projection of the source AST and can be edited
|
|
21
|
+
without duplicating slide state. Users do not get arbitrary element placement;
|
|
22
|
+
reconsider only for a later format that can represent positions explicitly.
|