src/poem-bars.lua
1-- poem-bars.lua
2--
3-- The box-drawing for poem entries: the top progress bar, the corner-boxed
4-- nav line (│ similar │ ... │ different │), and the bottom bar with junctions
5-- (╘══╧══╧══┘). Extracted from flat-html-generator's main-thread originals so
6-- the parallel effil workers can require the SAME code instead of carrying a
7-- drifted inline copy. That drift is what produced 88-char bars with doubled
8-- ╧╧ junctions on similar/different pages while the main-thread chronological
9-- pages stayed correct at 83. One module -> one bar -> no drift.
10--
11-- Geometry (regular poems, 83 chars wide, 0-indexed):
12-- left corner box : positions 0-10 (┌ + 9×─ + ┐ , walls at 0 and 10)
13-- gap : positions 11-69 (59 spaces)
14-- right corner box : positions 70-82 (┌ + 11×─ + ┐, walls at 70 and 82)
15-- Bottom-bar junctions sit UNDER the inner walls, at positions 10 and 70.
16-- Golden poems are 84 wide (2 outer ║ walls); the right junction shifts to 71.
17
18local M = {}
19
20-- Color config (name -> hex). Injected by the host so this module needs no
21-- knowledge of the project's palette. configure() is called once per Lua state
22-- (main thread and each worker).
23local color_config = {}
24
25-- {{{ function M.configure()
26function M.configure(cfg)
27 color_config = cfg or {}
28end
29-- }}}
30
31-- {{{ function M.colorize_char()
32function M.colorize_char(char, hex_color)
33 if hex_color then
34 return string.format('<font color="%s"><b>%s</b></font>', hex_color, char)
35 end
36 return char
37end
38-- }}}
39
40local colorize_char = M.colorize_char
41
42-- {{{ function M.progress_dashes()
43-- The top/bottom separator bar. position is "top" or "bottom"; has_corner_boxes
44-- inserts the ╧/┴ junctions that connect a bottom bar to the nav corner boxes.
45-- Returns { visual = <html>, accessibility = <aria-label attr> }.
46function M.progress_dashes(progress_info, color_name, is_golden, position, has_corner_boxes)
47 local total_chars = is_golden and 82 or 83
48 local progress_chars = math.floor((progress_info.percentage / 100) * total_chars)
49 local remaining_chars = total_chars - progress_chars
50
51 local hex_color = color_config[color_name] or color_config["gray"]
52
53 local LEFT_JUNCTION_POS = 10
54 local RIGHT_JUNCTION_POS = 71 -- golden: right inner wall (1 wider)
55 local REGULAR_LEFT_JUNCTION_POS = 10
56 local REGULAR_RIGHT_JUNCTION_POS = 70
57
58 local visual_output
59 if is_golden and position == "bottom" and has_corner_boxes then
60 local left_in_progress = LEFT_JUNCTION_POS < progress_chars
61 local right_in_progress = RIGHT_JUNCTION_POS < progress_chars
62 -- LEFT junction is DOUBLE-up (╩ over ═, ╨ over ─) because the left nav
63 -- box is double-line; RIGHT junction is single-up (╧/┴) for the
64 -- single-line right box. This matches the golden ║-left / │-right frame.
65 -- Left junction is the fill frontier of the similar box: double-up (╩)
66 -- only when progress has passed column 10, single-up (┴) until then.
67 local left_junction = left_in_progress
68 and string.format('<font color="%s"><b>╩</b></font>', hex_color) or "┴"
69 local right_junction = right_in_progress
70 and string.format('<font color="%s"><b>╧</b></font>', hex_color) or "┴"
71
72 local segments = {}
73 local function add_segment(start_pos, end_pos)
74 if end_pos <= start_pos then return end
75 local seg_len = end_pos - start_pos
76 local progress_in_seg = math.max(0, math.min(seg_len, progress_chars - start_pos))
77 local remaining_in_seg = seg_len - progress_in_seg
78 if progress_in_seg > 0 then
79 segments[#segments + 1] = string.format('<font color="%s"><b>%s</b></font>',
80 hex_color, string.rep("═", progress_in_seg))
81 end
82 if remaining_in_seg > 0 then
83 segments[#segments + 1] = string.rep("─", remaining_in_seg)
84 end
85 end
86
87 -- Junctions sit at columns 10 and 71, under the nav-box corners. The ╚
88 -- corner is column 0 so the first dash segment starts at 1; the last
89 -- segment runs to total_chars+1 so the interior is exactly 82 wide and
90 -- the junctions line up with the separator above (the reflection).
91 add_segment(1, LEFT_JUNCTION_POS)
92 segments[#segments + 1] = left_junction
93 add_segment(LEFT_JUNCTION_POS + 1, RIGHT_JUNCTION_POS)
94 segments[#segments + 1] = right_junction
95 add_segment(RIGHT_JUNCTION_POS + 1, total_chars + 1)
96
97 local colored_corner = string.format('<font color="%s"><b>╚</b></font>', hex_color)
98 visual_output = colored_corner .. table.concat(segments, "") .. "┘"
99
100 elseif not is_golden and position == "bottom" and has_corner_boxes then
101 local left_in_progress = REGULAR_LEFT_JUNCTION_POS < progress_chars
102 local right_in_progress = REGULAR_RIGHT_JUNCTION_POS < progress_chars
103 local left_junction = left_in_progress
104 and string.format('<font color="%s"><b>╧</b></font>', hex_color) or "┴"
105 local right_junction = right_in_progress
106 and string.format('<font color="%s"><b>╧</b></font>', hex_color) or "┴"
107 local left_corner = progress_chars > 0
108 and string.format('<font color="%s"><b>╘</b></font>', hex_color) or "╘"
109 local right_corner = "┘"
110
111 local segments = { left_corner }
112 local function add_segment(start_pos, end_pos)
113 if end_pos <= start_pos then return end
114 local seg_len = end_pos - start_pos
115 local progress_in_seg = math.max(0, math.min(seg_len, progress_chars - start_pos))
116 local remaining_in_seg = seg_len - progress_in_seg
117 if progress_in_seg > 0 then
118 segments[#segments + 1] = string.format('<font color="%s"><b>%s</b></font>',
119 hex_color, string.rep("═", progress_in_seg))
120 end
121 if remaining_in_seg > 0 then
122 segments[#segments + 1] = string.rep("─", remaining_in_seg)
123 end
124 end
125
126 add_segment(1, REGULAR_LEFT_JUNCTION_POS)
127 segments[#segments + 1] = left_junction
128 add_segment(REGULAR_LEFT_JUNCTION_POS + 1, REGULAR_RIGHT_JUNCTION_POS)
129 segments[#segments + 1] = right_junction
130 add_segment(REGULAR_RIGHT_JUNCTION_POS + 1, total_chars - 1)
131 segments[#segments + 1] = right_corner
132 visual_output = table.concat(segments, "")
133
134 elseif is_golden then
135 local progress_section = string.rep("═", progress_chars)
136 local remaining_section = string.rep("─", remaining_chars)
137 local colored_progress = string.format('<font color="%s"><b>%s</b></font>%s',
138 hex_color, progress_section, remaining_section)
139 local colored_top_corner = string.format('<font color="%s"><b>╔</b></font>', hex_color)
140 local colored_bottom_corner = string.format('<font color="%s"><b>╚</b></font>', hex_color)
141 if position == "top" then
142 visual_output = colored_top_corner .. colored_progress .. "┐"
143 else
144 visual_output = colored_bottom_corner .. colored_progress .. "┘"
145 end
146 else
147 local progress_section = string.rep("═", progress_chars)
148 local remaining_section = string.rep("─", remaining_chars)
149 visual_output = string.format('<font color="%s"><b>%s</b></font>%s',
150 hex_color, progress_section, remaining_section)
151 end
152
153 local screen_reader_text = is_golden
154 and string.format('aria-label="golden poem border. %s."', color_name)
155 or string.format('aria-label="eighty dashes. %s."', color_name)
156
157 return {
158 visual = visual_output,
159 accessibility = screen_reader_text,
160 raw_progress = progress_chars,
161 raw_remaining = remaining_chars,
162 color = color_name,
163 percentage = progress_info.percentage,
164 is_golden = is_golden or false,
165 }
166end
167-- }}}
168
169-- {{{ function M.corner_box_top()
170-- Top line of the nav corner boxes for REGULAR poems: ┌──┐ ... ┌────┐ (83 wide).
171function M.corner_box_top(progress_chars, hex_color)
172 progress_chars = progress_chars or 0
173 local left = {}
174 left[#left + 1] = progress_chars > 0 and colorize_char("┌", hex_color) or "┌"
175 for i = 1, 9 do left[#left + 1] = progress_chars > i and colorize_char("─", hex_color) or "─" end
176 left[#left + 1] = progress_chars > 10 and colorize_char("┐", hex_color) or "┐"
177 local gap = string.rep(" ", 59)
178 local right = {}
179 right[#right + 1] = progress_chars > 70 and colorize_char("┌", hex_color) or "┌"
180 for i = 71, 81 do right[#right + 1] = progress_chars > i and colorize_char("─", hex_color) or "─" end
181 right[#right + 1] = progress_chars > 82 and colorize_char("┐", hex_color) or "┐"
182 return table.concat(left) .. gap .. table.concat(right)
183end
184-- }}}
185
186-- {{{ function M.corner_box_nav_line()
187-- The │ similar │ ... chronological ... │ different │ line for REGULAR poems
188-- (83 wide). chronological_link is nil on the chronological page itself.
189function M.corner_box_nav_line(similar_link, different_link, chronological_link, progress_chars, hex_color)
190 progress_chars = progress_chars or 0
191 local similar_visible = similar_link:gsub("<[^>]+>", "")
192 local different_visible = different_link:gsub("<[^>]+>", "")
193
194 local center_text, center_visible_len = "", 0
195 if chronological_link then
196 center_text = chronological_link
197 center_visible_len = chronological_link:gsub("<[^>]+>", ""):len()
198 end
199
200 local left_wall = progress_chars > 0 and colorize_char("│", hex_color) or "│"
201 local right_wall_of_left = progress_chars > 10 and colorize_char("│", hex_color) or "│"
202 local similar_padding = 9 - 1 - #similar_visible
203 local left_box = left_wall .. " " .. similar_link .. string.rep(" ", similar_padding) .. right_wall_of_left
204
205 local left_wall_of_right = progress_chars > 70 and colorize_char("│", hex_color) or "│"
206 local right_wall = progress_chars > 82 and colorize_char("│", hex_color) or "│"
207 local different_padding = 11 - 1 - #different_visible
208 local right_box = left_wall_of_right .. " " .. different_link .. string.rep(" ", different_padding) .. right_wall
209
210 local left_gap, right_gap
211 if center_visible_len > 0 then
212 left_gap, right_gap = string.rep(" ", 23), string.rep(" ", 23)
213 else
214 left_gap, right_gap = string.rep(" ", 29), string.rep(" ", 30)
215 end
216 return left_box .. left_gap .. center_text .. right_gap .. right_box
217end
218-- }}}
219
220-- {{{ function M.corner_box_bottom()
221function M.corner_box_bottom()
222 return "└" .. string.rep("─", 9) .. "┘" .. string.rep(" ", 59) .. "└" .. string.rep("─", 11) .. "┘"
223end
224-- }}}
225
226-- {{{ function M.golden_corner_box_separator()
227-- GOLDEN poem nav separator (84 wide): ╠═════════╗ ... ┌───────────┤
228-- The LEFT box is DOUBLE-line (╠ + 9×═ + ╗) because it fuses with the golden
229-- poem's ║ outer wall; the RIGHT box is single-line (┌ + 11×─ + ┤) because the
230-- golden poem's right wall is │. 60-space gap between.
231function M.golden_corner_box_separator(hex_color, progress_chars)
232 progress_chars = progress_chars or 84 -- default: fully filled (back-compat)
233 -- Each dash MIRRORS the progress bar at that column: ═ (double, colored)
234 -- where progress has reached, ─ (single) where it has not -- so the box top
235 -- reads as a continuation of the bar. Corners are structural: ╠ joins the
236 -- ║ frame, ╗ caps the double-line left box, ┌/┤ the single-line right box.
237 local function dash(pos)
238 return (progress_chars > pos) and colorize_char("═", hex_color) or "─"
239 end
240 local left = { colorize_char("╠", hex_color) }
241 for i = 1, 9 do left[#left + 1] = dash(i) end
242 -- Right corner of the similar box is the FILL FRONTIER: double (╗) only once
243 -- progress has swept past column 10, single (┐) until then.
244 left[#left + 1] = (progress_chars > 10) and colorize_char("╗", hex_color) or "┐"
245 local right = { (progress_chars > 70) and colorize_char("┌", hex_color) or "┌" }
246 for i = 71, 81 do right[#right + 1] = dash(i) end
247 right[#right + 1] = (progress_chars > 82) and colorize_char("┤", hex_color) or "┤"
248 return table.concat(left) .. string.rep(" ", 60) .. table.concat(right)
249end
250-- }}}
251
252-- {{{ function M.golden_corner_box_nav_line()
253-- GOLDEN poem nav line (84 wide): ║ similar ║ ... chronological ... │ different │
254-- Left box ║...║ = 11 (double-line, joins the ║ frame), right box │...│ = 13
255-- (single-line); gaps 22+25 with center, 30+30 without.
256function M.golden_corner_box_nav_line(similar_link, different_link, chronological_link, hex_color, progress_chars)
257 progress_chars = progress_chars or 84 -- default fully filled (back-compat)
258 local similar_visible = similar_link:gsub("<[^>]+>", "")
259 local different_visible = different_link:gsub("<[^>]+>", "")
260 local center_text, center_visible_len = "", 0
261 if chronological_link then
262 center_text = chronological_link
263 center_visible_len = chronological_link:gsub("<[^>]+>", ""):len()
264 end
265 local colored_wall = string.format('<font color="%s"><b>║</b></font>', hex_color)
266 -- Left wall joins the ║ frame (always double). The RIGHT wall is the fill
267 -- frontier: double ║ once progress passes column 10, single │ until then.
268 local right_wall_of_left = (progress_chars > 10) and colored_wall or "│"
269 local left_box = colored_wall .. " " .. similar_link .. string.rep(" ", 9 - 1 - #similar_visible) .. right_wall_of_left
270 local right_box = "│ " .. different_link .. string.rep(" ", 11 - 1 - #different_visible) .. "│"
271 local left_gap, right_gap
272 if center_visible_len > 0 then
273 left_gap, right_gap = string.rep(" ", 22), string.rep(" ", 25)
274 else
275 left_gap, right_gap = string.rep(" ", 30), string.rep(" ", 30)
276 end
277 return left_box .. left_gap .. center_text .. right_gap .. right_box
278end
279-- }}}
280
281return M
282