ligarb 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.
@@ -0,0 +1,335 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= language %>">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= title %></title>
7
+ <style>
8
+ <%= css %>
9
+ </style>
10
+ <%- if custom_css -%>
11
+ <style>
12
+ <%= custom_css %>
13
+ </style>
14
+ <%- end -%>
15
+ <%- if assets.need?(:highlight) -%>
16
+ <link rel="stylesheet" href="css/highlight.css">
17
+ <%- end -%>
18
+ <%- if assets.need?(:katex) -%>
19
+ <link rel="stylesheet" href="css/katex.min.css">
20
+ <%- end -%>
21
+ </head>
22
+ <body>
23
+
24
+ <div class="sidebar" id="sidebar">
25
+ <div class="sidebar-header">
26
+ <div class="sidebar-header-top">
27
+ <%- cover_chapter = chapters.find(&:cover?) -%>
28
+ <%- if cover_chapter -%>
29
+ <h1 class="book-title"><a href="#<%= cover_chapter.slug %>" onclick="showChapter('<%= cover_chapter.slug %>'); return false;"><%= title %></a></h1>
30
+ <%- else -%>
31
+ <h1 class="book-title"><%= title %></h1>
32
+ <%- end -%>
33
+ <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">&#9790;</button>
34
+ </div>
35
+ <%- unless author.empty? -%>
36
+ <p class="book-author"><%= author %></p>
37
+ <%- end -%>
38
+ </div>
39
+ <div class="search-box">
40
+ <input type="text" id="toc-search" placeholder="Search..." autocomplete="off">
41
+ <button class="search-clear" id="search-clear" type="button" aria-label="Clear search">&times;</button>
42
+ </div>
43
+ <nav class="toc" id="toc">
44
+ <ul>
45
+ <%- structure.each do |node| -%>
46
+ <%- case node.type -%>
47
+ <%- when :cover -%>
48
+ <%- when :chapter -%>
49
+ <li class="toc-chapter" data-chapter="<%= node.chapter.slug %>">
50
+ <a href="#<%= node.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= node.chapter.slug %>')"><%= node.chapter.display_title %></a>
51
+ <%- sub_headings = node.chapter.headings.select { |h| h.level >= 2 } -%>
52
+ <%- unless sub_headings.empty? -%>
53
+ <ul>
54
+ <%- sub_headings.each do |heading| -%>
55
+ <li>
56
+ <a href="#<%= node.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>')"><%= heading.display_text %></a>
57
+ </li>
58
+ <%- end -%>
59
+ </ul>
60
+ <%- end -%>
61
+ </li>
62
+ <%- when :part -%>
63
+ <li class="toc-part">
64
+ <a href="#<%= node.chapter.slug %>" class="toc-part-title" onclick="showChapter('<%= node.chapter.slug %>')"><%= node.chapter.title %></a>
65
+ <ul>
66
+ <%- (node.children || []).each do |child| -%>
67
+ <li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
68
+ <a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= child.chapter.display_title %></a>
69
+ <%- sub_headings = child.chapter.headings.select { |h| h.level >= 2 } -%>
70
+ <%- unless sub_headings.empty? -%>
71
+ <ul>
72
+ <%- sub_headings.each do |heading| -%>
73
+ <li>
74
+ <a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= heading.display_text %></a>
75
+ </li>
76
+ <%- end -%>
77
+ </ul>
78
+ <%- end -%>
79
+ </li>
80
+ <%- end -%>
81
+ </ul>
82
+ </li>
83
+ <%- when :appendix_group -%>
84
+ <li class="toc-appendix">
85
+ <span class="toc-appendix-title"><%= appendix_label %></span>
86
+ <ul>
87
+ <%- (node.children || []).each do |child| -%>
88
+ <li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
89
+ <a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= child.chapter.display_title %></a>
90
+ <%- sub_headings = child.chapter.headings.select { |h| h.level >= 2 } -%>
91
+ <%- unless sub_headings.empty? -%>
92
+ <ul>
93
+ <%- sub_headings.each do |heading| -%>
94
+ <li>
95
+ <a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= heading.display_text %></a>
96
+ </li>
97
+ <%- end -%>
98
+ </ul>
99
+ <%- end -%>
100
+ </li>
101
+ <%- end -%>
102
+ </ul>
103
+ </li>
104
+ <%- end -%>
105
+ <%- end -%>
106
+ </ul>
107
+ </nav>
108
+ </div>
109
+
110
+ <button class="sidebar-toggle" id="sidebar-toggle" aria-label="Toggle sidebar">&#9776;</button>
111
+
112
+ <main class="content" id="content">
113
+ <%- chapters.each_with_index do |chapter, idx| -%>
114
+ <section class="<%= chapter.cover? ? 'chapter cover-page' : 'chapter' %>" id="chapter-<%= chapter.slug %>" style="display: none;">
115
+ <%= chapter.html %>
116
+ <%- if repository && !chapter.part_title? && !chapter.cover? -%>
117
+ <div class="edit-link">
118
+ <a href="<%= repository.chomp('/') %>/blob/HEAD/<%= chapter.relative_path %>" target="_blank" rel="noopener">View on GitHub</a>
119
+ </div>
120
+ <%- end -%>
121
+ <%- unless chapter.part_title? || chapter.cover? -%>
122
+ <nav class="chapter-nav">
123
+ <%- if idx > 0 -%>
124
+ <a href="#" class="nav-prev" onclick="showChapter('<%= chapters[idx-1].slug %>'); return false;">&larr; <%= chapters[idx-1].display_title %></a>
125
+ <%- else -%>
126
+ <span></span>
127
+ <%- end -%>
128
+ <%- if idx < chapters.size - 1 -%>
129
+ <a href="#" class="nav-next" onclick="showChapter('<%= chapters[idx+1].slug %>'); return false;"><%= chapters[idx+1].display_title %> &rarr;</a>
130
+ <%- end -%>
131
+ </nav>
132
+ <%- end -%>
133
+ </section>
134
+ <%- end -%>
135
+ </main>
136
+
137
+ <script>
138
+ (function() {
139
+ var chapters = [<%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %>];
140
+ var currentChapter = null;
141
+
142
+ function showChapter(slug) {
143
+ chapters.forEach(function(ch) {
144
+ var el = document.getElementById('chapter-' + ch);
145
+ if (el) el.style.display = (ch === slug) ? 'block' : 'none';
146
+ });
147
+
148
+ // Update active state in TOC
149
+ var tocItems = document.querySelectorAll('.toc-chapter');
150
+ tocItems.forEach(function(item) {
151
+ if (item.dataset.chapter === slug) {
152
+ item.classList.add('active');
153
+ } else {
154
+ item.classList.remove('active');
155
+ }
156
+ });
157
+
158
+ currentChapter = slug;
159
+ history.replaceState(null, '', '#' + slug);
160
+ window.scrollTo(0, 0);
161
+
162
+ // Re-apply highlight if search is active
163
+ if (searchInput && searchInput.value) {
164
+ highlightContent(searchInput.value);
165
+ }
166
+ }
167
+
168
+ function showChapterAndScroll(slug, headingId) {
169
+ showChapter(slug);
170
+ history.replaceState(null, '', '#' + headingId);
171
+ setTimeout(function() {
172
+ var target = document.getElementById(headingId);
173
+ if (target) target.scrollIntoView({ behavior: 'smooth' });
174
+ }, 50);
175
+ }
176
+
177
+ // Handle initial hash
178
+ function handleHash() {
179
+ var hash = location.hash.replace('#', '');
180
+ if (!hash) {
181
+ if (chapters.length > 0) showChapter(chapters[0]);
182
+ return;
183
+ }
184
+
185
+ // Check if hash matches a chapter slug directly
186
+ if (chapters.indexOf(hash) !== -1) {
187
+ showChapter(hash);
188
+ return;
189
+ }
190
+
191
+ // Check for deep link (chapter--heading)
192
+ var parts = hash.split('--');
193
+ if (parts.length >= 2) {
194
+ var chSlug = parts[0];
195
+ if (chapters.indexOf(chSlug) !== -1) {
196
+ showChapterAndScroll(chSlug, hash);
197
+ return;
198
+ }
199
+ }
200
+
201
+ // Fallback
202
+ if (chapters.length > 0) showChapter(chapters[0]);
203
+ }
204
+
205
+ // TOC search + content highlight
206
+ var searchInput = document.getElementById('toc-search');
207
+ var clearBtn = document.getElementById('search-clear');
208
+
209
+ function updateSearch() {
210
+ var query = searchInput.value.toLowerCase();
211
+ var items = document.querySelectorAll('.toc-chapter');
212
+ items.forEach(function(item) {
213
+ var text = item.textContent.toLowerCase();
214
+ item.style.display = text.indexOf(query) !== -1 ? '' : 'none';
215
+ });
216
+ clearBtn.style.display = query ? 'block' : 'none';
217
+ highlightContent(searchInput.value);
218
+ }
219
+
220
+ searchInput.addEventListener('input', updateSearch);
221
+
222
+ clearBtn.addEventListener('click', function() {
223
+ searchInput.value = '';
224
+ updateSearch();
225
+ searchInput.focus();
226
+ });
227
+
228
+ // Content highlight
229
+ function highlightContent(query) {
230
+ // Remove existing highlights
231
+ var marks = document.querySelectorAll('mark.search-highlight');
232
+ marks.forEach(function(mark) {
233
+ var parent = mark.parentNode;
234
+ parent.replaceChild(document.createTextNode(mark.textContent), mark);
235
+ parent.normalize();
236
+ });
237
+
238
+ if (!query || query.length < 2) return;
239
+
240
+ // Highlight in current chapter
241
+ if (!currentChapter) return;
242
+ var section = document.getElementById('chapter-' + currentChapter);
243
+ if (!section) return;
244
+
245
+ highlightInNode(section, query.toLowerCase());
246
+ }
247
+
248
+ function highlightInNode(node, query) {
249
+ if (node.nodeType === 3) { // Text node
250
+ var text = node.textContent;
251
+ var lower = text.toLowerCase();
252
+ var idx = lower.indexOf(query);
253
+ if (idx === -1) return;
254
+
255
+ var frag = document.createDocumentFragment();
256
+ var lastIdx = 0;
257
+ while (idx !== -1) {
258
+ frag.appendChild(document.createTextNode(text.substring(lastIdx, idx)));
259
+ var mark = document.createElement('mark');
260
+ mark.className = 'search-highlight';
261
+ mark.textContent = text.substring(idx, idx + query.length);
262
+ frag.appendChild(mark);
263
+ lastIdx = idx + query.length;
264
+ idx = lower.indexOf(query, lastIdx);
265
+ }
266
+ frag.appendChild(document.createTextNode(text.substring(lastIdx)));
267
+ node.parentNode.replaceChild(frag, node);
268
+ } else if (node.nodeType === 1 && node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE' && node.tagName !== 'MARK' && node.tagName !== 'PRE') {
269
+ // Collect children into array first (live NodeList changes during iteration)
270
+ var children = Array.prototype.slice.call(node.childNodes);
271
+ children.forEach(function(child) { highlightInNode(child, query); });
272
+ }
273
+ }
274
+
275
+ // Sidebar toggle for mobile
276
+ var toggle = document.getElementById('sidebar-toggle');
277
+ var sidebar = document.getElementById('sidebar');
278
+ toggle.addEventListener('click', function() {
279
+ sidebar.classList.toggle('open');
280
+ });
281
+
282
+ // Close sidebar when clicking a link on mobile
283
+ var tocLinks = document.querySelectorAll('.toc a');
284
+ tocLinks.forEach(function(link) {
285
+ link.addEventListener('click', function() {
286
+ if (window.innerWidth <= 768) {
287
+ sidebar.classList.remove('open');
288
+ }
289
+ });
290
+ });
291
+
292
+ // Dark mode toggle
293
+ var themeToggle = document.getElementById('theme-toggle');
294
+ var savedTheme = localStorage.getItem('ligarb-theme');
295
+ if (savedTheme) {
296
+ document.documentElement.setAttribute('data-theme', savedTheme);
297
+ }
298
+ themeToggle.addEventListener('click', function() {
299
+ var current = document.documentElement.getAttribute('data-theme');
300
+ var next = current === 'dark' ? 'light' : 'dark';
301
+ document.documentElement.setAttribute('data-theme', next);
302
+ localStorage.setItem('ligarb-theme', next);
303
+ });
304
+
305
+ // Expose to onclick handlers
306
+ window.showChapter = showChapter;
307
+ window.showChapterAndScroll = showChapterAndScroll;
308
+
309
+ // Initialize
310
+ handleHash();
311
+ window.addEventListener('hashchange', handleHash);
312
+ })();
313
+ </script>
314
+ <%- if assets.need?(:highlight) -%>
315
+ <script src="js/highlight.min.js"></script>
316
+ <script>hljs.highlightAll();</script>
317
+ <%- end -%>
318
+ <%- if assets.need?(:mermaid) -%>
319
+ <script src="js/mermaid.min.js"></script>
320
+ <script>
321
+ mermaid.initialize({startOnLoad: false});
322
+ mermaid.run({querySelector: '.mermaid'});
323
+ </script>
324
+ <%- end -%>
325
+ <%- if assets.need?(:katex) -%>
326
+ <script src="js/katex.min.js"></script>
327
+ <script>
328
+ document.querySelectorAll('.math-block').forEach(function(el) {
329
+ try { katex.render(el.getAttribute('data-math'), el, {displayMode: true, throwOnError: false}); }
330
+ catch(e) { el.textContent = el.getAttribute('data-math'); }
331
+ });
332
+ </script>
333
+ <%- end -%>
334
+ </body>
335
+ </html>
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ligarb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ligarb contributors
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: kramdown
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.4'
26
+ - !ruby/object:Gem::Dependency
27
+ name: kramdown-parser-gfm
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: A CLI tool that converts multiple Markdown files into a self-contained
69
+ index.html with a searchable table of contents sidebar and chapter navigation.
70
+ executables:
71
+ - ligarb
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - assets/style.css
76
+ - exe/ligarb
77
+ - lib/ligarb/asset_manager.rb
78
+ - lib/ligarb/builder.rb
79
+ - lib/ligarb/chapter.rb
80
+ - lib/ligarb/cli.rb
81
+ - lib/ligarb/config.rb
82
+ - lib/ligarb/initializer.rb
83
+ - lib/ligarb/template.rb
84
+ - lib/ligarb/version.rb
85
+ - templates/book.html.erb
86
+ homepage: https://github.com/ligarb/ligarb
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '3.0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 4.0.3
105
+ specification_version: 4
106
+ summary: Generate a single-page HTML book from Markdown files
107
+ test_files: []