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 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
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-23
4
+
5
+ - Initial release.
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,11 @@
1
+ {
2
+ "name": "Midnight",
3
+ "colors": {
4
+ "background": "#111820",
5
+ "text": "#edf2f7",
6
+ "muted": "#aab7c4",
7
+ "accent": "#79b8ff"
8
+ },
9
+ "font": { "family": "sans-serif", "title_size": 44, "body_size": 26 },
10
+ "spacing": { "margin": 64, "gap": 24 }
11
+ }
@@ -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,11 @@
1
+ {
2
+ "name": "Paper",
3
+ "colors": {
4
+ "background": "#fbf5e9",
5
+ "text": "#302820",
6
+ "muted": "#75695b",
7
+ "accent": "#9c4f35"
8
+ },
9
+ "font": { "family": "sans-serif", "title_size": 46, "body_size": 27 },
10
+ "spacing": { "margin": 68, "gap": 26 }
11
+ }
@@ -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.