libs/hope-card-formatter.lua
1-- hope-card-formatter.lua
2-- Formats poems for words-pdf input (80-dash separated text format)
3--
4-- This module converts neocities poem data into the format expected by
5-- the words-pdf project: plain text poems separated by 80 dashes.
6--
7-- INPUT: Table of poem objects from poems.json
8-- OUTPUT: String formatted for words-pdf (or written to file)
9
10local M = {}
11
12-- {{{ M.DASH_SEPARATOR
13M.DASH_SEPARATOR = string.rep("-", 80)
14-- }}}
15
16-- {{{ M.format_poem_for_wordspdf
17-- Formats a single poem for words-pdf output
18-- @param poem: Poem object from poems.json
19-- @return string: Formatted poem text
20function M.format_poem_for_wordspdf(poem)
21 if not poem then return "" end
22
23 local lines = {}
24
25 -- Add poem content lines
26 if poem.content then
27 -- Content is a single string, split by newlines
28 for line in poem.content:gmatch("[^\n]+") do
29 -- Ensure line doesn't exceed 80 characters
30 if #line > 80 then
31 -- Word wrap long lines
32 local wrapped = M.wrap_line(line, 80)
33 for _, wrapped_line in ipairs(wrapped) do
34 table.insert(lines, wrapped_line)
35 end
36 else
37 table.insert(lines, line)
38 end
39 end
40 elseif poem.lines then
41 -- Content is already split into lines
42 for _, line in ipairs(poem.lines) do
43 if #line > 80 then
44 local wrapped = M.wrap_line(line, 80)
45 for _, wrapped_line in ipairs(wrapped) do
46 table.insert(lines, wrapped_line)
47 end
48 else
49 table.insert(lines, line)
50 end
51 end
52 end
53
54 return table.concat(lines, "\n")
55end
56-- }}}
57
58-- {{{ M.wrap_line
59-- Wraps a line that exceeds max_width by breaking at word boundaries
60-- @param line: String to wrap
61-- @param max_width: Maximum characters per line (default 80)
62-- @return table: Array of wrapped lines
63function M.wrap_line(line, max_width)
64 max_width = max_width or 80
65
66 if #line <= max_width then
67 return {line}
68 end
69
70 local wrapped = {}
71 local current = ""
72
73 -- Split on whitespace and rebuild within width
74 for word in line:gmatch("%S+") do
75 if current == "" then
76 -- First word on line
77 if #word > max_width then
78 -- Single word exceeds width - hard break it
79 table.insert(wrapped, word:sub(1, max_width))
80 current = word:sub(max_width + 1)
81 else
82 current = word
83 end
84 elseif #current + 1 + #word <= max_width then
85 -- Word fits on current line
86 current = current .. " " .. word
87 else
88 -- Start new line
89 table.insert(wrapped, current)
90 current = word
91 end
92 end
93
94 -- Add remaining text
95 if current ~= "" then
96 table.insert(wrapped, current)
97 end
98
99 return wrapped
100end
101-- }}}
102
103-- {{{ M.format_poems_to_string
104-- Formats multiple poems into a single words-pdf compatible string
105-- @param poems: Array of poem objects
106-- @return string: Complete formatted text with 80-dash separators
107function M.format_poems_to_string(poems)
108 if not poems or #poems == 0 then
109 return ""
110 end
111
112 local output_parts = {}
113
114 for i, poem in ipairs(poems) do
115 local formatted = M.format_poem_for_wordspdf(poem)
116 table.insert(output_parts, formatted)
117
118 -- Add separator after each poem (including the last one)
119 table.insert(output_parts, M.DASH_SEPARATOR)
120 end
121
122 return table.concat(output_parts, "\n")
123end
124-- }}}
125
126-- {{{ M.write_to_file
127-- Writes formatted poems to a file for words-pdf consumption
128-- @param poems: Array of poem objects
129-- @param output_path: File path to write to
130-- @return boolean: Success status
131-- @return string: Error message if failed
132function M.write_to_file(poems, output_path)
133 local formatted_text = M.format_poems_to_string(poems)
134
135 local file, err = io.open(output_path, "w")
136 if not file then
137 return false, "Cannot open file for writing: " .. (err or "unknown error")
138 end
139
140 file:write(formatted_text)
141 file:close()
142
143 return true, nil
144end
145-- }}}
146
147-- {{{ M.generate_hope_card_filename
148-- Generates a descriptive filename for a hope card
149-- @param anchor_id: The anchor poem ID
150-- @param poem_count: Number of poems in the card
151-- @return string: Filename (without path)
152function M.generate_hope_card_filename(anchor_id, poem_count)
153 return string.format("hope-card-%04d-%dpoems.txt", anchor_id, poem_count)
154end
155-- }}}
156
157return M
158