Skip to content

Commit d662724

Browse files
committed
refactor: タイムテーブルのロジックをJekyllプラグインに移行
- Liquidテンプレートから時間計算をプラグインとして実装して簡略化 - Liquidテンプレートから複雑なロジックを除去し、表示のみに専念 - 時間計算のロジックと、描画のテンプレートの可読性をそれぞれ改善
1 parent 86e39e9 commit d662724

File tree

4 files changed

+197
-75
lines changed

4 files changed

+197
-75
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ group :jekyll_plugins do
1212
gem 'jekyll-sitemap'
1313
gem 'jekyll-liquify'
1414
gem 'jekyll-redirect-from'
15+
gem 'activesupport' # For time calculations in plugins
1516

1617
# No need this gem because we build by GitHub Actions and serve on Pages.
1718
# gem 'github-pages'

Gemfile.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@ GEM
22
remote: https://rubygems.org/
33
specs:
44
Ascii85 (2.0.1)
5+
activesupport (8.0.2.1)
6+
base64
7+
benchmark (>= 0.3)
8+
bigdecimal
9+
concurrent-ruby (~> 1.0, >= 1.3.1)
10+
connection_pool (>= 2.2.5)
11+
drb
12+
i18n (>= 1.6, < 2)
13+
logger (>= 1.4.2)
14+
minitest (>= 5.1)
15+
securerandom (>= 0.3)
16+
tzinfo (~> 2.0, >= 2.0.5)
17+
uri (>= 0.13.1)
518
addressable (2.8.7)
619
public_suffix (>= 2.0.2, < 7.0)
720
afm (0.2.2)
@@ -12,14 +25,17 @@ GEM
1225
metrics (~> 0.12)
1326
traces (~> 0.15)
1427
base64 (0.2.0)
28+
benchmark (0.4.1)
1529
bigdecimal (3.1.9)
1630
colorator (1.1.0)
1731
concurrent-ruby (1.3.5)
32+
connection_pool (2.5.4)
1833
console (1.30.2)
1934
fiber-annotation
2035
fiber-local (~> 1.1)
2136
json
2237
csv (3.3.4)
38+
drb (2.2.3)
2339
em-websocket (0.5.3)
2440
eventmachine (>= 0.12.9)
2541
http_parser.rb (~> 0)
@@ -132,6 +148,7 @@ GEM
132148
metrics (0.12.2)
133149
mini_racer (0.18.1)
134150
libv8-node (~> 23.6.1.0)
151+
minitest (5.25.5)
135152
nokogiri (1.18.9-aarch64-linux-gnu)
136153
racc (~> 1.4)
137154
nokogiri (1.18.9-aarch64-linux-musl)
@@ -185,6 +202,7 @@ GEM
185202
google-protobuf (~> 4.31)
186203
sass-embedded (1.89.0-x86_64-linux-musl)
187204
google-protobuf (~> 4.31)
205+
securerandom (0.4.1)
188206
terminal-table (3.0.2)
189207
unicode-display_width (>= 1.1.1, < 3)
190208
traces (0.15.2)
@@ -195,7 +213,10 @@ GEM
195213
bigdecimal (~> 3.1)
196214
typhoeus (1.4.1)
197215
ethon (>= 0.9.0)
216+
tzinfo (2.0.6)
217+
concurrent-ruby (~> 1.0)
198218
unicode-display_width (2.6.0)
219+
uri (1.0.3)
199220
webrick (1.9.1)
200221
yell (2.2.2)
201222
zeitwerk (2.7.3)
@@ -215,6 +236,7 @@ PLATFORMS
215236
x86_64-linux-musl
216237

217238
DEPENDENCIES
239+
activesupport
218240
html-proofer
219241
jekyll
220242
jekyll-feed

_pages/time-table.html

Lines changed: 45 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
---
66
{% include navbar.html %}
77

8-
{% assign tt = site.data.time_table %}
9-
{% assign slot = tt.slot_minutes | default: 15 %}
10-
{% assign rooms = tt.rooms %}
11-
{% assign room_count = rooms | size %}
8+
{% comment %}
9+
Jekyll プラグインで事前計算されたタイムテーブル表を使用
10+
ロジックはプラグインで計算済み。Liquid は描画のみを担当
11+
{% endcomment %}
1212

13-
{% assign day_start_min = 600 %}
14-
{% assign day_end_min = 960 %}
15-
{% assign total_minutes = day_end_min | minus: day_start_min %}
16-
{% assign slots_count = total_minutes | divided_by: slot %}
17-
{% assign last_row = slots_count | minus: 1 %}
13+
{% assign ttg = site.data.time_table_grid %}
14+
{% assign time_slots = ttg.time_slots %}
15+
{% assign rooms = ttg.rooms %}
16+
{% assign room_styles = ttg.room_styles %}
17+
{% assign time_labels = ttg.time_labels %}
1818

1919
<section class="max-w-[1200px] mx-auto px-4 sm:px-8 mt-30 xl:mt-15">
2020
<h2 class="text-4xl text-center mb-8">
@@ -23,94 +23,64 @@ <h2 class="text-4xl text-center mb-8">
2323
</h2>
2424

2525
<div class="ttable-wrap" aria-label="タイムテーブル(横スクロール可)">
26-
<table class="ttable" style="--room-count: {{ room_count }};">
26+
<table class="ttable" style="--room-count: {{ rooms | size }};">
2727
<caption>
28-
{{ tt.date | default: site.date_event }} のタイムテーブル
28+
{{ site.date_event }} のタイムテーブル
2929
</caption>
3030

3131
<thead>
3232
<tr>
3333
<th scope="col" class="ttable__th ttable__th--start">時間</th>
34-
{% for r in rooms %}
35-
{% assign rstyle = tt.room_styles[r] %}
34+
{% for room in rooms %}
35+
{% assign rstyle = room_styles[room] %}
3636
<th scope="col"
3737
class="ttable__th ttable__th--room"
3838
style="--room-color: {{ rstyle.color | default: '#c43b3b' }};">
39-
<span class="ttable__room-cap">{{ r }}</span>
39+
<span class="ttable__room-cap">{{ room }}</span>
4040
</th>
4141
{% endfor %}
4242
</tr>
4343
</thead>
4444

4545
<tbody>
46-
{% for i in (0..last_row) %}
47-
{% assign row_min = i | times: slot | plus: day_start_min %}
48-
{% assign h = row_min | divided_by: 60 %}
49-
{% assign mf = row_min | modulo: 60 | plus: 0 | prepend: '0' | slice: -2, 2 %}
46+
{% for time_slot in time_slots %}
47+
{% assign slot_index = forloop.index0 %}
48+
{% assign time_label = time_labels[slot_index] %}
5049

5150
<tr>
52-
<!-- 左1列(sticky) -->
53-
<th scope="row" class="ttable__cell ttable__cell--start">{{ h }}:{{ mf }}</th>
51+
<th scope="row" class="ttable__cell ttable__cell--start">{{ time_label }}</th>
5452

55-
{% for r in rooms %}
56-
{% assign rstyle = tt.room_styles[r] %}
57-
{% assign events_in_room = tt.events | where: 'room', r | sort: 'start' %}
58-
59-
{% assign active_event = nil %}
60-
{% assign active_event_start_index = nil %}
61-
62-
{% for ev in events_in_room %}
63-
{% assign s_h = ev.start | split: ':' | first | plus: 0 %}
64-
{% assign s_m = ev.start | split: ':' | last | plus: 0 %}
65-
{% assign e_h = ev.end | split: ':' | first | plus: 0 %}
66-
{% assign e_m = ev.end | split: ':' | last | plus: 0 %}
67-
{% assign s_min = s_h | times: 60 | plus: s_m %}
68-
{% assign e_min = e_h | times: 60 | plus: e_m %}
69-
70-
{% assign s_clamped = s_min %}
71-
{% if s_clamped < day_start_min %}{% assign s_clamped = day_start_min %}{% endif %}
72-
{% assign e_clamped = e_min %}
73-
{% if e_clamped > day_end_min %}{% assign e_clamped = day_end_min %}{% endif %}
74-
75-
{% assign span_minutes = e_clamped | minus: s_clamped %}
76-
{% if span_minutes > 0 %}
77-
{% assign numerator = span_minutes | plus: slot | minus: 1 %}
78-
{% assign span_slots = numerator | divided_by: slot %}
79-
{% assign s_index = s_clamped | minus: day_start_min | divided_by: slot %}
80-
{% assign e_index = s_index | plus: span_slots %}
81-
{% if i >= s_index and i < e_index %}
82-
{% assign active_event = ev %}
83-
{% assign active_event_start_index = s_index %}
84-
{% endif %}
85-
{% endif %}
86-
{% endfor %}
87-
88-
{% if active_event and i == active_event_start_index %}
89-
{%- assign s_h = active_event.start | split: ':' | first | plus: 0 -%}
90-
{%- assign s_m = active_event.start | split: ':' | last | plus: 0 -%}
91-
{%- assign e_h = active_event.end | split: ':' | first | plus: 0 -%}
92-
{%- assign e_m = active_event.end | split: ':' | last | plus: 0 -%}
93-
{%- assign s_min = s_h | times: 60 | plus: s_m -%}
94-
{%- assign e_min = e_h | times: 60 | plus: e_m -%}
95-
{%- if s_min < day_start_min -%}{%- assign s_min = day_start_min -%}{%- endif -%}
96-
{%- if e_min > day_end_min -%}{%- assign e_min = day_end_min -%}{%- endif -%}
97-
{%- assign span_minutes = e_min | minus: s_min -%}
98-
{%- assign numerator = span_minutes | plus: slot | minus: 1 -%}
99-
{%- assign span_slots = numerator | divided_by: slot -%}
100-
{%- assign accent = active_event.accent | default: rstyle.color -%}
101-
<td class="ttable__cell ttable__cell--event" rowspan="{{ span_slots }}" style="--span: {{ span_slots }};">
102-
<div class="ttable__event" style="--accent: {{ accent | default: '#c43b3b' }};">
103-
<div class="ttable__event-time">{{ active_event.start }}–{{ active_event.end }}</div>
104-
<div class="ttable__event-title">{{ active_event.title }}</div>
105-
{% if active_event.subtitle %}
106-
<div class="ttable__event-subtitle">{{ active_event.subtitle }}</div>
53+
{% for room_event in time_slot %}
54+
{% assign room_index = forloop.index0 %}
55+
{% assign room = rooms[room_index] %}
56+
{% assign rstyle = room_styles[room] %}
57+
58+
{% if room_event.event %}
59+
{% comment %} イベントの開始セル {% endcomment %}
60+
{% assign event_data = room_event.event %}
61+
{% assign accent = event_data.accent | default: rstyle.color | default: '#c43b3b' %}
62+
63+
<td class="ttable__cell ttable__cell--event"
64+
rowspan="{{ room_event.span }}"
65+
style="--span: {{ room_event.span }};">
66+
<div class="ttable__event" style="--accent: {{ accent }};">
67+
<div class="ttable__event-time">{{ event_data.start }}–{{ event_data.end }}</div>
68+
<div class="ttable__event-title">{{ event_data.title }}</div>
69+
{% if event_data.subtitle %}
70+
<div class="ttable__event-subtitle">{{ event_data.subtitle }}</div>
71+
{% endif %}
72+
{% if event_data.badge %}
73+
<span class="ttable__badge">{{ event_data.badge }}</span>
10774
{% endif %}
108-
{% if active_event.badge %}
109-
<span class="ttable__badge">{{ active_event.badge }}</span>
75+
{% if event_data.note %}
76+
<div class="ttable__event-note">{{ event_data.note }}</div>
11077
{% endif %}
11178
</div>
11279
</td>
113-
{% elsif active_event == nil %}
80+
{% elsif room_event.continued %}
81+
{% comment %} 継続イベント(rowspanでカバーされているので出力は不要) {% endcomment %}
82+
{% else %}
83+
{% comment %} 空きイベント {% endcomment %}
11484
<td class="ttable__cell ttable__cell--empty" aria-label="空き時間"></td>
11585
{% endif %}
11686
{% endfor %}

_plugins/time_table_generator.rb

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# frozen_string_literal: true
2+
3+
require 'active_support/core_ext/integer/time'
4+
require 'active_support/core_ext/numeric/time'
5+
6+
module Jekyll
7+
module TimeTableGenerator
8+
# タイムテーブル表を事前に計算してグリッド形式に変換
9+
# これにより、Liquid テンプレートは単純な表示のみを担当
10+
class Generator < Jekyll::Generator
11+
safe true
12+
priority :high
13+
14+
# デフォルト設定値
15+
DEFAULT_SLOT_MINUTES = 15
16+
DEFAULT_DAY_START_HOUR = 10 # 10:00
17+
DEFAULT_DAY_END_HOUR = 16 # 16:00
18+
19+
def generate(site)
20+
return unless site.data['time_table']
21+
22+
tt = site.data['time_table']
23+
24+
# 設定値(Active Support の Duration を使用)
25+
slot_duration = tt.fetch('slot_minutes', DEFAULT_SLOT_MINUTES).minutes
26+
day_start = tt.fetch('day_start_hour', DEFAULT_DAY_START_HOUR).hours
27+
day_end = tt.fetch('day_end_hour', DEFAULT_DAY_END_HOUR).hours
28+
total_duration = day_end - day_start
29+
total_slots = (total_duration / slot_duration).to_i
30+
31+
rooms = tt.fetch('rooms', [])
32+
events = tt.fetch('events', [])
33+
34+
# イベントグリッドを作成(行 = 時間スロット、列 = 部屋)
35+
event_grid = Array.new(total_slots) { Array.new(rooms.size) }
36+
37+
# イベントグリッドに各イベントを配置
38+
events.each do |event|
39+
place_event_on_grid(event, event_grid, rooms, day_start, day_end, slot_duration, total_slots)
40+
end
41+
42+
# 各スロットの時刻を事前計算
43+
time_labels = (0...total_slots).map do |slot_index|
44+
slot_time = day_start + (slot_index * slot_duration)
45+
format_time_label(slot_time)
46+
end
47+
48+
# 計算済みデータをsite.dataに追加(分単位に戻して保存)
49+
site.data['time_table_grid'] = {
50+
'time_slots' => event_grid, # より明確な名前:時間スロットの配列
51+
'rooms' => rooms,
52+
'room_styles' => tt.fetch('room_styles', {}),
53+
'time_labels' => time_labels,
54+
'slot_minutes' => slot_duration.in_minutes.to_i,
55+
'day_start_min' => day_start.in_minutes.to_i,
56+
'day_end_min' => day_end.in_minutes.to_i,
57+
'total_slots' => total_slots
58+
}
59+
end
60+
61+
private
62+
63+
def place_event_on_grid(event, event_grid, rooms, day_start, day_end, slot_duration, total_slots)
64+
room_index = rooms.index(event['room'])
65+
return unless room_index
66+
67+
# 開始・終了時間を Duration に変換
68+
start_time = parse_time_to_duration(event['start'])
69+
end_time = parse_time_to_duration(event['end'])
70+
71+
# 表示範囲内に収める(クリッピング)
72+
display_start = [start_time, day_start].max
73+
display_end = [end_time, day_end].min
74+
75+
# スロットインデックスを計算
76+
start_slot, end_slot, span = calculate_slot_indices(display_start, display_end, day_start, slot_duration)
77+
78+
# クリッピング済みなので start_slot は必ず有効範囲内
79+
# display_start は day_start 以上、day_end 以下に制限されている
80+
return if start_slot >= total_slots # 念のためのチェック(通常はテストで検知)
81+
82+
# イベント開始セルを配置
83+
event_grid[start_slot][room_index] = create_event_cell(event, span, start_slot, end_slot)
84+
85+
# 継続スロットにマーカーを配置
86+
mark_continued_slots(event_grid, room_index, start_slot, end_slot, total_slots)
87+
end
88+
89+
def calculate_slot_indices(display_start, display_end, day_start, slot_duration)
90+
start_slot = ((display_start - day_start) / slot_duration).to_i
91+
# 終了時刻が正確にスロット境界上の場合、そのスロットを含めない
92+
# 例: 10:30終了で15分スロットの場合、10:30-10:45のスロットは含めない
93+
end_slot = ((display_end - day_start) / slot_duration).ceil
94+
span = end_slot - start_slot
95+
[start_slot, end_slot, span]
96+
end
97+
98+
def create_event_cell(event, span, start_slot, end_slot)
99+
{
100+
'event' => event,
101+
'span' => span,
102+
'start_slot' => start_slot,
103+
'end_slot' => end_slot
104+
}
105+
end
106+
107+
def mark_continued_slots(event_grid, room_index, start_slot, end_slot, total_slots)
108+
# end_slot を有効範囲内に制限してから反復処理
109+
actual_end = [end_slot, total_slots].min
110+
(start_slot + 1...actual_end).each do |slot|
111+
event_grid[slot][room_index] = { 'continued' => true }
112+
end
113+
end
114+
115+
def parse_time_to_duration(time_str)
116+
return 0.hours unless time_str
117+
time = Time.parse(time_str)
118+
time.hour.hours + time.min.minutes
119+
end
120+
121+
def format_time_label(duration)
122+
total_minutes = duration.in_minutes.to_i
123+
hours = total_minutes / 60
124+
minutes = total_minutes % 60
125+
format('%d:%02d', hours, minutes)
126+
end
127+
end
128+
end
129+
end

0 commit comments

Comments
 (0)