ligarb 0.5.0 → 0.7.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.
@@ -1,9 +1,9 @@
1
1
  <!DOCTYPE html>
2
- <html lang="<%= language %>">
2
+ <html lang="<%= h(language) %>">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title><%= title %></title>
6
+ <title><%= h(title) %></title>
7
7
  <%- if ai_generated -%>
8
8
  <meta name="robots" content="noindex, nofollow, noarchive">
9
9
  <meta name="robots" content="noai, noimageai">
@@ -28,39 +28,156 @@
28
28
  <div class="sidebar" id="sidebar">
29
29
  <div class="sidebar-header">
30
30
  <div class="sidebar-header-top">
31
+ <%- if multilang -%>
32
+ <%- langs.each do |ld| -%>
33
+ <%- ld_cover = ld[:chapters].find(&:cover?) -%>
34
+ <%- if ld_cover -%>
35
+ <h1 class="book-title lang-content" data-lang="<%= ld[:lang] %>"><a href="#<%= ld_cover.slug %>" onclick="showChapter('<%= ld_cover.slug %>'); return false;"><%= h(ld[:title]) %></a></h1>
36
+ <%- else -%>
37
+ <h1 class="book-title lang-content" data-lang="<%= ld[:lang] %>"><%= h(ld[:title]) %></h1>
38
+ <%- end -%>
39
+ <%- end -%>
40
+ <%- else -%>
31
41
  <%- cover_chapter = chapters.find(&:cover?) -%>
32
42
  <%- if cover_chapter -%>
33
- <h1 class="book-title"><a href="#<%= cover_chapter.slug %>" onclick="showChapter('<%= cover_chapter.slug %>'); return false;"><%= title %></a></h1>
43
+ <h1 class="book-title"><a href="#<%= cover_chapter.slug %>" onclick="showChapter('<%= cover_chapter.slug %>'); return false;"><%= h(title) %></a></h1>
34
44
  <%- else -%>
35
- <h1 class="book-title"><%= title %></h1>
45
+ <h1 class="book-title"><%= h(title) %></h1>
36
46
  <%- end -%>
47
+ <%- end -%>
37
48
  <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">&#9790;</button>
38
49
  </div>
50
+ <%- if multilang -%>
51
+ <%- langs.each do |ld| -%>
52
+ <%- unless ld[:author].empty? -%>
53
+ <p class="book-author lang-content" data-lang="<%= ld[:lang] %>"><%= h(ld[:author]) %></p>
54
+ <%- end -%>
55
+ <%- end -%>
56
+ <%- else -%>
39
57
  <%- unless author.empty? -%>
40
- <p class="book-author"><%= author %></p>
58
+ <p class="book-author"><%= h(author) %></p>
59
+ <%- end -%>
60
+ <%- end -%>
61
+ <%- if multilang -%>
62
+ <%- if ai_generated -%>
63
+ <%- langs.each do |ld| -%>
64
+ <span class="ai-badge lang-content" data-lang="<%= ld[:lang] %>"><%= ld[:language] == 'ja' ? 'AI 生成' : 'AI Generated' %></span>
41
65
  <%- end -%>
66
+ <%- end -%>
67
+ <%- else -%>
42
68
  <%- if ai_generated -%>
43
69
  <span class="ai-badge"><%= language == 'ja' ? 'AI 生成' : 'AI Generated' %></span>
44
70
  <%- end -%>
71
+ <%- end -%>
72
+ <%- if multilang -%>
73
+ <div class="lang-switcher">
74
+ <%- langs.each do |ld| -%>
75
+ <button class="lang-link" data-lang="<%= ld[:lang] %>" onclick="switchLang('<%= ld[:lang] %>')"><%= h(ld[:lang].upcase) %></button>
76
+ <%- end -%>
77
+ </div>
78
+ <%- end -%>
45
79
  </div>
46
80
  <div class="search-box">
47
81
  <input type="text" id="toc-search" placeholder="Search..." autocomplete="off">
48
82
  <button class="search-clear" id="search-clear" type="button" aria-label="Clear search">&times;</button>
49
83
  </div>
50
84
  <nav class="toc" id="toc">
85
+ <%- if multilang -%>
86
+ <%- langs.each do |ld| -%>
87
+ <ul class="lang-content" data-lang="<%= ld[:lang] %>">
88
+ <li class="toc-chapter" data-chapter="<%= ld[:lang] %>--__toc__">
89
+ <a href="#<%= ld[:lang] %>--__toc__" class="toc-h1" onclick="showChapter('<%= ld[:lang] %>--__toc__')"><%= ld[:language] == 'ja' ? '目次' : 'Contents' %></a>
90
+ </li>
91
+ <%- ld[:structure].each do |node| -%>
92
+ <%- case node.type -%>
93
+ <%- when :cover -%>
94
+ <%- when :chapter -%>
95
+ <li class="toc-chapter" data-chapter="<%= node.chapter.slug %>">
96
+ <a href="#<%= node.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.display_title) %></a>
97
+ <%- sub_headings = node.chapter.headings.select { |h| h.level == 2 } -%>
98
+ <%- unless sub_headings.empty? -%>
99
+ <ul>
100
+ <%- sub_headings.each do |heading| -%>
101
+ <li>
102
+ <a href="#<%= node.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
103
+ </li>
104
+ <%- end -%>
105
+ </ul>
106
+ <%- end -%>
107
+ </li>
108
+ <%- when :part -%>
109
+ <li class="toc-part">
110
+ <a href="#<%= node.chapter.slug %>" class="toc-part-title" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.title) %></a>
111
+ <ul>
112
+ <%- (node.children || []).each do |child| -%>
113
+ <li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
114
+ <a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
115
+ <%- sub_headings = child.chapter.headings.select { |h| h.level == 2 } -%>
116
+ <%- unless sub_headings.empty? -%>
117
+ <ul>
118
+ <%- sub_headings.each do |heading| -%>
119
+ <li>
120
+ <a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
121
+ </li>
122
+ <%- end -%>
123
+ </ul>
124
+ <%- end -%>
125
+ </li>
126
+ <%- end -%>
127
+ </ul>
128
+ </li>
129
+ <%- when :appendix_group -%>
130
+ <li class="toc-appendix">
131
+ <span class="toc-appendix-title"><%= h(ld[:appendix_label]) %></span>
132
+ <ul>
133
+ <%- (node.children || []).each do |child| -%>
134
+ <li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
135
+ <a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
136
+ <%- sub_headings = child.chapter.headings.select { |h| h.level == 2 } -%>
137
+ <%- unless sub_headings.empty? -%>
138
+ <ul>
139
+ <%- sub_headings.each do |heading| -%>
140
+ <li>
141
+ <a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
142
+ </li>
143
+ <%- end -%>
144
+ </ul>
145
+ <%- end -%>
146
+ </li>
147
+ <%- end -%>
148
+ </ul>
149
+ </li>
150
+ <%- end -%>
151
+ <%- end -%>
152
+ <%- unless ld[:bibliography].empty? -%>
153
+ <li class="toc-chapter" data-chapter="<%= ld[:lang] %>--__bibliography__">
154
+ <a href="#<%= ld[:lang] %>--__bibliography__" class="toc-h1" onclick="showChapter('<%= ld[:lang] %>--__bibliography__')"><%= ld[:language] == 'ja' ? '参考文献' : 'Bibliography' %></a>
155
+ </li>
156
+ <%- end -%>
157
+ <%- unless ld[:index_tree].empty? -%>
158
+ <li class="toc-chapter" data-chapter="<%= ld[:lang] %>--__index__">
159
+ <a href="#<%= ld[:lang] %>--__index__" class="toc-h1" onclick="showChapter('<%= ld[:lang] %>--__index__')"><%= ld[:language] == 'ja' ? '索引' : 'Index' %></a>
160
+ </li>
161
+ <%- end -%>
162
+ </ul>
163
+ <%- end -%>
164
+ <%- else -%>
51
165
  <ul>
166
+ <li class="toc-chapter" data-chapter="__toc__">
167
+ <a href="#__toc__" class="toc-h1" onclick="showChapter('__toc__')"><%= language == 'ja' ? '目次' : 'Contents' %></a>
168
+ </li>
52
169
  <%- structure.each do |node| -%>
53
170
  <%- case node.type -%>
54
171
  <%- when :cover -%>
55
172
  <%- when :chapter -%>
56
173
  <li class="toc-chapter" data-chapter="<%= node.chapter.slug %>">
57
- <a href="#<%= node.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= node.chapter.slug %>')"><%= node.chapter.display_title %></a>
58
- <%- sub_headings = node.chapter.headings.select { |h| h.level >= 2 } -%>
174
+ <a href="#<%= node.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.display_title) %></a>
175
+ <%- sub_headings = node.chapter.headings.select { |h| h.level == 2 } -%>
59
176
  <%- unless sub_headings.empty? -%>
60
177
  <ul>
61
178
  <%- sub_headings.each do |heading| -%>
62
179
  <li>
63
- <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>
180
+ <a href="#<%= node.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
64
181
  </li>
65
182
  <%- end -%>
66
183
  </ul>
@@ -68,17 +185,17 @@
68
185
  </li>
69
186
  <%- when :part -%>
70
187
  <li class="toc-part">
71
- <a href="#<%= node.chapter.slug %>" class="toc-part-title" onclick="showChapter('<%= node.chapter.slug %>')"><%= node.chapter.title %></a>
188
+ <a href="#<%= node.chapter.slug %>" class="toc-part-title" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.title) %></a>
72
189
  <ul>
73
190
  <%- (node.children || []).each do |child| -%>
74
191
  <li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
75
- <a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= child.chapter.display_title %></a>
76
- <%- sub_headings = child.chapter.headings.select { |h| h.level >= 2 } -%>
192
+ <a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
193
+ <%- sub_headings = child.chapter.headings.select { |h| h.level == 2 } -%>
77
194
  <%- unless sub_headings.empty? -%>
78
195
  <ul>
79
196
  <%- sub_headings.each do |heading| -%>
80
197
  <li>
81
- <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>
198
+ <a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
82
199
  </li>
83
200
  <%- end -%>
84
201
  </ul>
@@ -89,17 +206,17 @@
89
206
  </li>
90
207
  <%- when :appendix_group -%>
91
208
  <li class="toc-appendix">
92
- <span class="toc-appendix-title"><%= appendix_label %></span>
209
+ <span class="toc-appendix-title"><%= h(appendix_label) %></span>
93
210
  <ul>
94
211
  <%- (node.children || []).each do |child| -%>
95
212
  <li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
96
- <a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= child.chapter.display_title %></a>
97
- <%- sub_headings = child.chapter.headings.select { |h| h.level >= 2 } -%>
213
+ <a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
214
+ <%- sub_headings = child.chapter.headings.select { |h| h.level == 2 } -%>
98
215
  <%- unless sub_headings.empty? -%>
99
216
  <ul>
100
217
  <%- sub_headings.each do |heading| -%>
101
218
  <li>
102
- <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>
219
+ <a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
103
220
  </li>
104
221
  <%- end -%>
105
222
  </ul>
@@ -121,32 +238,196 @@
121
238
  </li>
122
239
  <%- end -%>
123
240
  </ul>
241
+ <%- end -%>
124
242
  </nav>
125
243
  </div>
126
244
 
127
245
  <button class="sidebar-toggle" id="sidebar-toggle" aria-label="Toggle sidebar">&#9776;</button>
128
246
 
129
247
  <main class="content" id="content">
248
+ <%- if multilang -%>
249
+ <%- langs.each do |ld| -%>
250
+ <section class="chapter toc-page" id="chapter-<%= ld[:lang] %>--__toc__" data-lang="<%= ld[:lang] %>" style="display: none;">
251
+ <h1><%= ld[:language] == 'ja' ? '目次' : 'Contents' %></h1>
252
+ <nav class="toc-full">
253
+ <%- ld[:structure].each do |node| -%>
254
+ <%- case node.type -%>
255
+ <%- when :cover -%>
256
+ <%- when :chapter -%>
257
+ <div class="toc-full-chapter">
258
+ <a href="#<%= node.chapter.slug %>" onclick="showChapter('<%= node.chapter.slug %>'); return false;"><%= h(node.chapter.display_title) %></a>
259
+ <%- sub = node.chapter.headings.select { |h| h.level >= 2 } -%>
260
+ <%- unless sub.empty? -%>
261
+ <ul><%- sub.each do |heading| -%>
262
+ <li class="toc-full-h<%= heading.level %>"><a href="#<%= node.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
263
+ <%- end -%></ul>
264
+ <%- end -%>
265
+ </div>
266
+ <%- when :part -%>
267
+ <div class="toc-full-part">
268
+ <a href="#<%= node.chapter.slug %>" onclick="showChapter('<%= node.chapter.slug %>'); return false;" class="toc-full-part-title"><%= h(node.chapter.title) %></a>
269
+ <%- (node.children || []).each do |child| -%>
270
+ <div class="toc-full-chapter">
271
+ <a href="#<%= child.chapter.slug %>" onclick="showChapter('<%= child.chapter.slug %>'); return false;"><%= h(child.chapter.display_title) %></a>
272
+ <%- sub = child.chapter.headings.select { |h| h.level >= 2 } -%>
273
+ <%- unless sub.empty? -%>
274
+ <ul><%- sub.each do |heading| -%>
275
+ <li class="toc-full-h<%= heading.level %>"><a href="#<%= child.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
276
+ <%- end -%></ul>
277
+ <%- end -%>
278
+ </div>
279
+ <%- end -%>
280
+ </div>
281
+ <%- when :appendix_group -%>
282
+ <div class="toc-full-part">
283
+ <span class="toc-full-part-title"><%= h(ld[:appendix_label]) %></span>
284
+ <%- (node.children || []).each do |child| -%>
285
+ <div class="toc-full-chapter">
286
+ <a href="#<%= child.chapter.slug %>" onclick="showChapter('<%= child.chapter.slug %>'); return false;"><%= h(child.chapter.display_title) %></a>
287
+ <%- sub = child.chapter.headings.select { |h| h.level >= 2 } -%>
288
+ <%- unless sub.empty? -%>
289
+ <ul><%- sub.each do |heading| -%>
290
+ <li class="toc-full-h<%= heading.level %>"><a href="#<%= child.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
291
+ <%- end -%></ul>
292
+ <%- end -%>
293
+ </div>
294
+ <%- end -%>
295
+ </div>
296
+ <%- end -%>
297
+ <%- end -%>
298
+ </nav>
299
+ </section>
300
+ <%- ld[:chapters].each_with_index do |chapter, idx| -%>
301
+ <section class="<%= chapter.cover? ? 'chapter cover-page' : 'chapter' %>" id="chapter-<%= chapter.slug %>" data-lang="<%= ld[:lang] %>" style="display: none;">
302
+ <%= chapter.html %>
303
+ <%- if ld[:repository] && !chapter.part_title? && !chapter.cover? -%>
304
+ <div class="edit-link">
305
+ <a href="<%= h(ld[:repository].chomp('/')) %>/blob/HEAD/<%= h(chapter.relative_path) %>" target="_blank" rel="noopener">View on GitHub</a>
306
+ </div>
307
+ <%- end -%>
308
+ <%- if ld[:footer] && !chapter.part_title? && !chapter.cover? -%>
309
+ <div class="chapter-footer"><%= h(ld[:footer]) %></div>
310
+ <%- end -%>
311
+ <%- unless chapter.cover? -%>
312
+ <nav class="chapter-nav">
313
+ <%- if idx > 0 -%>
314
+ <a href="#" class="nav-prev" onclick="showChapter('<%= ld[:chapters][idx-1].slug %>'); return false;">&larr; <%= h(ld[:chapters][idx-1].display_title) %></a>
315
+ <%- else -%>
316
+ <span></span>
317
+ <%- end -%>
318
+ <%- if idx < ld[:chapters].size - 1 -%>
319
+ <a href="#" class="nav-next" onclick="showChapter('<%= ld[:chapters][idx+1].slug %>'); return false;"><%= h(ld[:chapters][idx+1].display_title) %> &rarr;</a>
320
+ <%- end -%>
321
+ </nav>
322
+ <%- end -%>
323
+ </section>
324
+ <%- end -%>
325
+ <%- unless ld[:bibliography].empty? -%>
326
+ <section class="chapter bibliography-chapter" id="chapter-<%= ld[:lang] %>--__bibliography__" data-lang="<%= ld[:lang] %>" style="display: none;">
327
+ <h1><%= ld[:language] == 'ja' ? '参考文献' : 'Bibliography' %></h1>
328
+ <ul class="bibliography-list">
329
+ <%- ld[:bibliography].each do |entry| -%>
330
+ <li id="<%= ld[:lang] %>--bib-<%= h(entry[:key]) %>"><span class="bib-label">[<%= h(entry[:label]) %>]</span> <%= entry[:formatted_html] %></li>
331
+ <%- end -%>
332
+ </ul>
333
+ </section>
334
+ <%- end -%>
335
+ <%- unless ld[:index_tree].empty? -%>
336
+ <section class="chapter index-chapter" id="chapter-<%= ld[:lang] %>--__index__" data-lang="<%= ld[:lang] %>" style="display: none;">
337
+ <h1><%= ld[:language] == 'ja' ? '索引' : 'Index' %></h1>
338
+ <%- ld[:index_tree].keys.sort.each do |letter| -%>
339
+ <div class="index-group">
340
+ <h2 class="index-letter"><%= letter %></h2>
341
+ <dl class="index-entries">
342
+ <%- ld[:index_tree][letter].each do |item| -%>
343
+ <dt><%= h(item[:term]) %></dt>
344
+ <%- item[:refs].each do |ref| -%>
345
+ <dd><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
346
+ <%- end -%>
347
+ <%- item[:children].each do |child| -%>
348
+ <dt class="index-sub"><%= h(child[:term]) %></dt>
349
+ <%- child[:refs].each do |ref| -%>
350
+ <dd class="index-sub"><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
351
+ <%- end -%>
352
+ <%- end -%>
353
+ <%- end -%>
354
+ </dl>
355
+ </div>
356
+ <%- end -%>
357
+ </section>
358
+ <%- end -%>
359
+ <%- end -%>
360
+ <%- else -%>
361
+ <section class="chapter toc-page" id="chapter-__toc__" style="display: none;">
362
+ <h1><%= language == 'ja' ? '目次' : 'Contents' %></h1>
363
+ <nav class="toc-full">
364
+ <%- structure.each do |node| -%>
365
+ <%- case node.type -%>
366
+ <%- when :cover -%>
367
+ <%- when :chapter -%>
368
+ <div class="toc-full-chapter">
369
+ <a href="#<%= node.chapter.slug %>" onclick="showChapter('<%= node.chapter.slug %>'); return false;"><%= h(node.chapter.display_title) %></a>
370
+ <%- sub = node.chapter.headings.select { |h| h.level >= 2 } -%>
371
+ <%- unless sub.empty? -%>
372
+ <ul><%- sub.each do |heading| -%>
373
+ <li class="toc-full-h<%= heading.level %>"><a href="#<%= node.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
374
+ <%- end -%></ul>
375
+ <%- end -%>
376
+ </div>
377
+ <%- when :part -%>
378
+ <div class="toc-full-part">
379
+ <a href="#<%= node.chapter.slug %>" onclick="showChapter('<%= node.chapter.slug %>'); return false;" class="toc-full-part-title"><%= h(node.chapter.title) %></a>
380
+ <%- (node.children || []).each do |child| -%>
381
+ <div class="toc-full-chapter">
382
+ <a href="#<%= child.chapter.slug %>" onclick="showChapter('<%= child.chapter.slug %>'); return false;"><%= h(child.chapter.display_title) %></a>
383
+ <%- sub = child.chapter.headings.select { |h| h.level >= 2 } -%>
384
+ <%- unless sub.empty? -%>
385
+ <ul><%- sub.each do |heading| -%>
386
+ <li class="toc-full-h<%= heading.level %>"><a href="#<%= child.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
387
+ <%- end -%></ul>
388
+ <%- end -%>
389
+ </div>
390
+ <%- end -%>
391
+ </div>
392
+ <%- when :appendix_group -%>
393
+ <div class="toc-full-part">
394
+ <span class="toc-full-part-title"><%= h(appendix_label) %></span>
395
+ <%- (node.children || []).each do |child| -%>
396
+ <div class="toc-full-chapter">
397
+ <a href="#<%= child.chapter.slug %>" onclick="showChapter('<%= child.chapter.slug %>'); return false;"><%= h(child.chapter.display_title) %></a>
398
+ <%- sub = child.chapter.headings.select { |h| h.level >= 2 } -%>
399
+ <%- unless sub.empty? -%>
400
+ <ul><%- sub.each do |heading| -%>
401
+ <li class="toc-full-h<%= heading.level %>"><a href="#<%= child.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
402
+ <%- end -%></ul>
403
+ <%- end -%>
404
+ </div>
405
+ <%- end -%>
406
+ </div>
407
+ <%- end -%>
408
+ <%- end -%>
409
+ </nav>
410
+ </section>
130
411
  <%- chapters.each_with_index do |chapter, idx| -%>
131
412
  <section class="<%= chapter.cover? ? 'chapter cover-page' : 'chapter' %>" id="chapter-<%= chapter.slug %>" style="display: none;">
132
413
  <%= chapter.html %>
133
414
  <%- if repository && !chapter.part_title? && !chapter.cover? -%>
134
415
  <div class="edit-link">
135
- <a href="<%= repository.chomp('/') %>/blob/HEAD/<%= chapter.relative_path %>" target="_blank" rel="noopener">View on GitHub</a>
416
+ <a href="<%= h(repository.chomp('/')) %>/blob/HEAD/<%= h(chapter.relative_path) %>" target="_blank" rel="noopener">View on GitHub</a>
136
417
  </div>
137
418
  <%- end -%>
138
419
  <%- if footer && !chapter.part_title? && !chapter.cover? -%>
139
- <div class="chapter-footer"><%= footer %></div>
420
+ <div class="chapter-footer"><%= h(footer) %></div>
140
421
  <%- end -%>
141
422
  <%- unless chapter.cover? -%>
142
423
  <nav class="chapter-nav">
143
424
  <%- if idx > 0 -%>
144
- <a href="#" class="nav-prev" onclick="showChapter('<%= chapters[idx-1].slug %>'); return false;">&larr; <%= chapters[idx-1].display_title %></a>
425
+ <a href="#" class="nav-prev" onclick="showChapter('<%= chapters[idx-1].slug %>'); return false;">&larr; <%= h(chapters[idx-1].display_title) %></a>
145
426
  <%- else -%>
146
427
  <span></span>
147
428
  <%- end -%>
148
429
  <%- if idx < chapters.size - 1 -%>
149
- <a href="#" class="nav-next" onclick="showChapter('<%= chapters[idx+1].slug %>'); return false;"><%= chapters[idx+1].display_title %> &rarr;</a>
430
+ <a href="#" class="nav-next" onclick="showChapter('<%= chapters[idx+1].slug %>'); return false;"><%= h(chapters[idx+1].display_title) %> &rarr;</a>
150
431
  <%- end -%>
151
432
  </nav>
152
433
  <%- end -%>
@@ -157,7 +438,7 @@
157
438
  <h1><%= language == 'ja' ? '参考文献' : 'Bibliography' %></h1>
158
439
  <ul class="bibliography-list">
159
440
  <%- bibliography.each do |entry| -%>
160
- <li id="bib-<%= entry[:key] %>"><span class="bib-label">[<%= entry[:label] %>]</span> <%= entry[:formatted_html] %></li>
441
+ <li id="bib-<%= h(entry[:key]) %>"><span class="bib-label">[<%= h(entry[:label]) %>]</span> <%= entry[:formatted_html] %></li>
161
442
  <%- end -%>
162
443
  </ul>
163
444
  </section>
@@ -170,12 +451,12 @@
170
451
  <h2 class="index-letter"><%= letter %></h2>
171
452
  <dl class="index-entries">
172
453
  <%- index_tree[letter].each do |item| -%>
173
- <dt><%= item[:term] %></dt>
454
+ <dt><%= h(item[:term]) %></dt>
174
455
  <%- item[:refs].each do |ref| -%>
175
456
  <dd><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
176
457
  <%- end -%>
177
458
  <%- item[:children].each do |child| -%>
178
- <dt class="index-sub"><%= child[:term] %></dt>
459
+ <dt class="index-sub"><%= h(child[:term]) %></dt>
179
460
  <%- child[:refs].each do |ref| -%>
180
461
  <dd class="index-sub"><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
181
462
  <%- end -%>
@@ -186,38 +467,180 @@
186
467
  <%- end -%>
187
468
  </section>
188
469
  <%- end -%>
470
+ <%- end -%>
189
471
  </main>
190
472
 
191
473
  <script>
192
474
  (function() {
193
- var chapters = [<%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ', "__bibliography__"' unless bibliography.empty? %><%= ', "__index__"' unless index_tree.empty? %>];
475
+ <%- if multilang -%>
476
+ var langChapters = {
477
+ <%- langs.each do |ld| -%>
478
+ "<%= ld[:lang] %>": ["<%= ld[:lang] %>--__toc__", <%= ld[:chapters].map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ", \"#{ld[:lang]}--__bibliography__\"" unless ld[:bibliography].empty? %><%= ", \"#{ld[:lang]}--__index__\"" unless ld[:index_tree].empty? %>],
479
+ <%- end -%>
480
+ };
481
+ var allLangs = [<%= langs.map { |ld| "\"#{ld[:lang]}\"" }.join(", ") %>];
482
+ var currentLang = localStorage.getItem('ligarb-lang') || allLangs[0];
483
+ if (allLangs.indexOf(currentLang) === -1) currentLang = allLangs[0];
484
+ var chapters = langChapters[currentLang];
485
+ <%- else -%>
486
+ var chapters = ["__toc__", <%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ', "__bibliography__"' unless bibliography.empty? %><%= ', "__index__"' unless index_tree.empty? %>];
487
+ <%- end -%>
194
488
  var currentChapter = null;
195
489
 
196
490
  var navigating = false; // flag to suppress pushState during popstate/initial load
197
491
 
492
+ // Track the currently visible heading index and highlight TOC
493
+ var currentHeadingIndex = -1;
494
+ var headingObserver = null;
495
+
496
+ function setupHeadingObserver() {
497
+ if (headingObserver) headingObserver.disconnect();
498
+ if (!currentChapter) return;
499
+ var section = document.getElementById('chapter-' + currentChapter);
500
+ if (!section) return;
501
+ var headings = section.querySelectorAll('h1, h2, h3');
502
+ if (headings.length === 0) return;
503
+
504
+ headingObserver = new IntersectionObserver(function(entries) {
505
+ entries.forEach(function(entry) {
506
+ if (entry.isIntersecting) {
507
+ var idx = Array.prototype.indexOf.call(headings, entry.target);
508
+ if (idx !== -1) {
509
+ currentHeadingIndex = idx;
510
+ highlightTocHeading(entry.target);
511
+ }
512
+ }
513
+ });
514
+ }, { rootMargin: '0px 0px -70% 0px', threshold: 0 });
515
+
516
+ headings.forEach(function(h) { headingObserver.observe(h); });
517
+ }
518
+
519
+ function highlightTocHeading(heading) {
520
+ // Remove previous active-section highlights
521
+ document.querySelectorAll('.toc a.active-section').forEach(function(a) {
522
+ a.classList.remove('active-section');
523
+ });
524
+ // Find the TOC link matching this heading's ID
525
+ var hid = heading.id;
526
+ if (!hid) return;
527
+ var tocLink = document.querySelector('.toc a[href="#' + CSS.escape(hid) + '"]');
528
+ if (tocLink) tocLink.classList.add('active-section');
529
+ }
530
+
531
+ <%- if multilang -%>
532
+
533
+ function switchLang(lang) {
534
+ // Remember current position: chapter base slug and heading index
535
+ var oldLang = currentLang;
536
+ var targetSlug = null;
537
+ var targetHeadingIdx = currentHeadingIndex;
538
+
539
+ if (currentChapter) {
540
+ // Strip lang prefix to get the base slug (e.g. "ja--03-book-yml" → "03-book-yml")
541
+ var baseSlug = currentChapter.replace(new RegExp('^' + oldLang + '--'), '');
542
+ targetSlug = lang + '--' + baseSlug;
543
+ }
544
+
545
+ currentLang = lang;
546
+ chapters = langChapters[lang];
547
+ localStorage.setItem('ligarb-lang', lang);
548
+
549
+ // Toggle lang-content elements (TOC, header)
550
+ document.querySelectorAll('.lang-content').forEach(function(el) {
551
+ el.style.display = el.dataset.lang === lang ? '' : 'none';
552
+ });
553
+
554
+ // Toggle lang-switcher buttons
555
+ document.querySelectorAll('.lang-link').forEach(function(btn) {
556
+ if (btn.dataset.lang === lang) {
557
+ btn.classList.add('lang-current');
558
+ } else {
559
+ btn.classList.remove('lang-current');
560
+ }
561
+ });
562
+
563
+ // Hide all chapter sections, then show first of current lang
564
+ document.querySelectorAll('main .chapter').forEach(function(el) {
565
+ el.style.display = 'none';
566
+ });
567
+
568
+ // Reset search cache
569
+ chapterTextCache = null;
570
+
571
+ // Navigate to the corresponding chapter in the new language
572
+ if (targetSlug && chapters.indexOf(targetSlug) !== -1) {
573
+ showChapter(targetSlug);
574
+ // Scroll to the corresponding heading by index
575
+ if (targetHeadingIdx >= 0) {
576
+ setTimeout(function() {
577
+ var section = document.getElementById('chapter-' + targetSlug);
578
+ if (!section) return;
579
+ var headings = section.querySelectorAll('h1, h2, h3');
580
+ if (targetHeadingIdx < headings.length) {
581
+ headings[targetHeadingIdx].scrollIntoView({ behavior: 'smooth' });
582
+ }
583
+ }, 50);
584
+ }
585
+ } else {
586
+ showChapter(chapters[0]);
587
+ }
588
+ }
589
+
590
+ // Initialize lang display
591
+ function initLang() {
592
+ document.querySelectorAll('.lang-content').forEach(function(el) {
593
+ el.style.display = el.dataset.lang === currentLang ? '' : 'none';
594
+ });
595
+ document.querySelectorAll('.lang-link').forEach(function(btn) {
596
+ if (btn.dataset.lang === currentLang) btn.classList.add('lang-current');
597
+ });
598
+ }
599
+
600
+ window.switchLang = switchLang;
601
+ <%- end -%>
602
+
198
603
  function showChapter(slug, hash) {
199
604
  chapters.forEach(function(ch) {
200
605
  var el = document.getElementById('chapter-' + ch);
201
606
  if (el) el.style.display = (ch === slug) ? 'block' : 'none';
202
607
  });
203
608
 
204
- // Update active state in TOC
609
+ // Update active state in TOC and scroll sidebar to show active item
610
+ <%- if multilang -%>
611
+ var tocContainer = document.querySelector('.toc .lang-content[data-lang="' + currentLang + '"]');
612
+ var tocItems = tocContainer ? tocContainer.querySelectorAll('.toc-chapter') : [];
613
+ <%- else -%>
205
614
  var tocItems = document.querySelectorAll('.toc-chapter');
615
+ <%- end -%>
616
+ var activeItem = null;
206
617
  tocItems.forEach(function(item) {
207
618
  if (item.dataset.chapter === slug) {
208
619
  item.classList.add('active');
620
+ activeItem = item;
209
621
  } else {
210
622
  item.classList.remove('active');
211
623
  }
212
624
  });
625
+ if (activeItem) {
626
+ var tocNav = document.getElementById('toc');
627
+ var itemRect = activeItem.getBoundingClientRect();
628
+ var tocRect = tocNav.getBoundingClientRect();
629
+ if (itemRect.top < tocRect.top || itemRect.bottom > tocRect.bottom) {
630
+ activeItem.scrollIntoView({ block: 'center' });
631
+ }
632
+ }
213
633
 
214
634
  currentChapter = slug;
635
+ currentHeadingIndex = -1;
215
636
  var newHash = '#' + (hash || slug);
216
637
  if (!navigating) {
217
638
  history.pushState(null, '', newHash);
218
639
  }
219
640
  window.scrollTo(0, 0);
220
641
 
642
+ setupHeadingObserver();
643
+
221
644
  // Re-apply highlight if search is active
222
645
  if (searchInput && searchInput.value) {
223
646
  highlightContent(searchInput.value);
@@ -232,7 +655,21 @@
232
655
  if (!section) return;
233
656
  if (typeof mermaid !== 'undefined') {
234
657
  var unrendered = section.querySelectorAll('.mermaid:not([data-processed])');
235
- if (unrendered.length > 0) mermaid.run({nodes: unrendered});
658
+ if (unrendered.length > 0) {
659
+ // Save original source for error recovery
660
+ var sources = {};
661
+ unrendered.forEach(function(el) { sources[el.id || el.textContent.slice(0, 50)] = el.textContent; });
662
+ mermaid.run({nodes: unrendered, suppressErrors: true}).catch(function() {}).finally(function() {
663
+ unrendered.forEach(function(el) {
664
+ // mermaid sets data-processed; if it's still text-only (no SVG), it failed
665
+ if (!el.querySelector('svg')) {
666
+ var src = sources[el.id || el.textContent.slice(0, 50)] || el.textContent;
667
+ el.innerHTML = '<pre style="color:#c00;border:1px solid #c00;padding:0.5em;white-space:pre-wrap">mermaid error:\n' +
668
+ src.replace(/</g, '&lt;') + '</pre>';
669
+ }
670
+ });
671
+ });
672
+ }
236
673
  }
237
674
  if (typeof katex !== 'undefined') {
238
675
  section.querySelectorAll('.math-block[data-math]').forEach(function(el) {
@@ -248,16 +685,94 @@
248
685
  }
249
686
  });
250
687
  }
688
+ if (typeof functionPlot !== 'undefined') {
689
+ section.querySelectorAll('.functionplot[data-plot]').forEach(function(el) {
690
+ if (el.childNodes.length > 0) return;
691
+ try {
692
+ var spec = el.getAttribute('data-plot');
693
+ var opts = parseFunctionPlotSpec(spec);
694
+ opts.target = el;
695
+ functionPlot(opts);
696
+ } catch(e) {
697
+ el.textContent = 'Error rendering plot: ' + e.message;
698
+ el.style.color = 'red';
699
+ }
700
+ });
701
+ }
702
+ }
703
+
704
+ // Parse functionplot spec: lines of "y = expr" and "key: value" options
705
+ function parseFunctionPlotSpec(spec) {
706
+ var lines = spec.split('\n');
707
+ var fns = [];
708
+ var opts = {width: 600, height: 400};
709
+ lines.forEach(function(line) {
710
+ line = line.trim();
711
+ if (!line) return;
712
+ // Option lines: key: value
713
+ var optMatch = line.match(/^(xrange|yrange|range|width|height|title|grid)\s*:\s*(.+)$/i);
714
+ if (optMatch) {
715
+ var key = optMatch[1].toLowerCase();
716
+ var val = optMatch[2].trim();
717
+ if (key === 'range' || key === 'xrange') {
718
+ opts.xAxis = {domain: parseRange(val)};
719
+ } else if (key === 'yrange') {
720
+ opts.yAxis = {domain: parseRange(val)};
721
+ } else if (key === 'width') {
722
+ opts.width = parseInt(val, 10);
723
+ } else if (key === 'height') {
724
+ opts.height = parseInt(val, 10);
725
+ } else if (key === 'title') {
726
+ opts.title = val;
727
+ } else if (key === 'grid') {
728
+ opts.grid = val === 'true';
729
+ }
730
+ return;
731
+ }
732
+ // Function lines: y = expr, r = expr (polar), x = expr (implicit), or bare expr
733
+ var fnMatch = line.match(/^y\s*=\s*(.+)$/);
734
+ if (fnMatch) { fns.push({fn: fnMatch[1].trim()}); return; }
735
+ var polarMatch = line.match(/^r\s*=\s*(.+)$/);
736
+ if (polarMatch) { fns.push({r: polarMatch[1].trim(), fnType: 'polar', graphType: 'polyline'}); return; }
737
+ var paramMatch = line.match(/^parametric\s*:\s*(.+?)\s*,\s*(.+)$/);
738
+ if (paramMatch) { fns.push({x: paramMatch[1].trim(), y: paramMatch[2].trim(), fnType: 'parametric', graphType: 'polyline'}); return; }
739
+ // Bare expression treated as y = expr
740
+ fns.push({fn: line});
741
+ });
742
+ opts.data = fns;
743
+ return opts;
744
+ }
745
+
746
+ function parseRange(str) {
747
+ var m = str.match(/\[?\s*(-?[\d.eE+piPI*\/]+)\s*,\s*(-?[\d.eE+piPI*\/]+)\s*\]?/);
748
+ if (!m) return [-10, 10];
749
+ return [evalRangeNum(m[1]), evalRangeNum(m[2])];
750
+ }
751
+
752
+ function evalRangeNum(s) {
753
+ s = s.replace(/pi/gi, String(Math.PI));
754
+ try { return Function('"use strict"; return (' + s + ')')(); }
755
+ catch(e) { return parseFloat(s) || 0; }
251
756
  }
252
757
 
253
758
  function showChapterAndScroll(slug, headingId) {
254
759
  showChapter(slug, headingId);
255
760
  setTimeout(function() {
256
761
  var target = document.getElementById(headingId);
257
- if (target) target.scrollIntoView({ behavior: 'smooth' });
762
+ if (target) {
763
+ target.scrollIntoView({ behavior: 'smooth' });
764
+ highlightElement(target);
765
+ highlightTocHeading(target);
766
+ }
258
767
  }, 50);
259
768
  }
260
769
 
770
+ function highlightElement(el) {
771
+ var prev = document.querySelector('.ligarb-highlight');
772
+ if (prev) prev.classList.remove('ligarb-highlight');
773
+ el.classList.add('ligarb-highlight');
774
+ }
775
+
261
776
  // Navigate to chapter/heading based on current URL hash
262
777
  function handleHash() {
263
778
  navigating = true;
@@ -268,6 +783,14 @@
268
783
  return;
269
784
  }
270
785
 
786
+ <%- if multilang -%>
787
+ // In multilang mode, detect language from hash prefix (e.g., "en--cover")
788
+ var langMatch = hash.match(/^([a-z]{2})--/);
789
+ if (langMatch && allLangs.indexOf(langMatch[1]) !== -1 && langMatch[1] !== currentLang) {
790
+ switchLang(langMatch[1]);
791
+ }
792
+ <%- end -%>
793
+
271
794
  // Check if hash matches a chapter slug directly
272
795
  if (chapters.indexOf(hash) !== -1) {
273
796
  showChapter(hash);
@@ -275,7 +798,20 @@
275
798
  return;
276
799
  }
277
800
 
278
- <%- unless bibliography.empty? -%>
801
+ <%- if multilang -%>
802
+ // Check for bibliography entry (bib-KEY)
803
+ var bibMatch = hash.match(/^(?:([a-z]{2})--)?bib-(.+)/);
804
+ if (bibMatch) {
805
+ var bibLang = bibMatch[1] || currentLang;
806
+ showChapter(bibLang + '--__bibliography__', hash);
807
+ setTimeout(function() {
808
+ var target = document.getElementById(hash);
809
+ if (target) target.scrollIntoView({ behavior: 'smooth' });
810
+ }, 50);
811
+ navigating = false;
812
+ return;
813
+ }
814
+ <%- elsif !bibliography.empty? -%>
279
815
  // Check for bibliography entry (bib-KEY)
280
816
  var bibMatch = hash.match(/^bib-(.+)/);
281
817
  if (bibMatch) {
@@ -287,7 +823,7 @@
287
823
  navigating = false;
288
824
  return;
289
825
  }
290
- <%- end -%>
826
+ <%- end -%>
291
827
 
292
828
  // Check for footnote links (fn: or fnref:)
293
829
  var fnMatch = hash.match(/^(?:fn|fnref):(.+?)--/);
@@ -305,12 +841,31 @@
305
841
  // Check for deep link (chapter--heading)
306
842
  var parts = hash.split('--');
307
843
  if (parts.length >= 2) {
844
+ <%- if multilang -%>
845
+ // In multilang, slug is "lang--chapter" so join first two parts
846
+ var chSlug = parts[0] + '--' + parts[1];
847
+ if (chapters.indexOf(chSlug) !== -1) {
848
+ showChapterAndScroll(chSlug, hash);
849
+ navigating = false;
850
+ return;
851
+ }
852
+ // Try 3 parts: "lang--chapter--heading" → chapter is "lang--chapter"
853
+ if (parts.length >= 3) {
854
+ chSlug = parts[0] + '--' + parts[1];
855
+ if (chapters.indexOf(chSlug) !== -1) {
856
+ showChapterAndScroll(chSlug, hash);
857
+ navigating = false;
858
+ return;
859
+ }
860
+ }
861
+ <%- else -%>
308
862
  var chSlug = parts[0];
309
863
  if (chapters.indexOf(chSlug) !== -1) {
310
864
  showChapterAndScroll(chSlug, hash);
311
865
  navigating = false;
312
866
  return;
313
867
  }
868
+ <%- end -%>
314
869
  }
315
870
 
316
871
  // Fallback
@@ -352,8 +907,14 @@
352
907
  // Remove existing match count badges
353
908
  document.querySelectorAll('.search-match-count').forEach(function(el) { el.remove(); });
354
909
 
910
+ <%- if multilang -%>
911
+ var tocContainer = document.querySelector('.toc .lang-content[data-lang="' + currentLang + '"]');
912
+ var items = tocContainer ? tocContainer.querySelectorAll('.toc-chapter') : [];
913
+ var partItems = tocContainer ? tocContainer.querySelectorAll('.toc-part, .toc-appendix') : [];
914
+ <%- else -%>
355
915
  var items = document.querySelectorAll('.toc-chapter');
356
916
  var partItems = document.querySelectorAll('.toc-part, .toc-appendix');
917
+ <%- end -%>
357
918
 
358
919
  if (!query || query.length < 2) {
359
920
  // Show all items when query is too short
@@ -520,6 +1081,9 @@
520
1081
  window.renderSpecialBlocks = function() { if (currentChapter) renderSpecialBlocks(currentChapter); };
521
1082
 
522
1083
  // Initialize (replace initial entry so first back goes to previous page, not same page)
1084
+ <%- if multilang -%>
1085
+ initLang();
1086
+ <%- end -%>
523
1087
  navigating = true;
524
1088
  handleHash();
525
1089
  history.replaceState(null, '', location.hash || '#' + (chapters[0] || ''));
@@ -542,5 +1106,10 @@ renderSpecialBlocks();
542
1106
  <script src="js/katex.min.js"></script>
543
1107
  <script>renderSpecialBlocks();</script>
544
1108
  <%- end -%>
1109
+ <%- if assets.need?(:functionplot) -%>
1110
+ <script src="js/d3.min.js"></script>
1111
+ <script src="js/function-plot.min.js"></script>
1112
+ <script>renderSpecialBlocks();</script>
1113
+ <%- end -%>
545
1114
  </body>
546
1115
  </html>