/src/glaze/include/glaze/stencil/stencil.hpp
Line | Count | Source (jump to first uncovered line) |
1 | | // Glaze Library |
2 | | // For the license information refer to glaze.hpp |
3 | | |
4 | | #pragma once |
5 | | |
6 | | #include "glaze/core/read.hpp" |
7 | | #include "glaze/core/reflect.hpp" |
8 | | #include "glaze/core/write.hpp" |
9 | | |
10 | | namespace glz |
11 | | { |
12 | | // HTML escape function |
13 | | inline std::string html_escape(const std::string& input) |
14 | 0 | { |
15 | 0 | std::string result; |
16 | 0 | result.reserve(static_cast<size_t>(input.size() * 1.1)); // Reserve some extra space |
17 | 0 |
|
18 | 0 | for (char c : input) { |
19 | 0 | switch (c) { |
20 | 0 | case '<': |
21 | 0 | result += "<"; |
22 | 0 | break; |
23 | 0 | case '>': |
24 | 0 | result += ">"; |
25 | 0 | break; |
26 | 0 | case '&': |
27 | 0 | result += "&"; |
28 | 0 | break; |
29 | 0 | case '"': |
30 | 0 | result += """; |
31 | 0 | break; |
32 | 0 | case '\'': |
33 | 0 | result += "'"; |
34 | 0 | break; |
35 | 0 | default: |
36 | 0 | result += c; |
37 | 0 | break; |
38 | 0 | } |
39 | 0 | } |
40 | 0 | return result; |
41 | 0 | } |
42 | | |
43 | | template <auto Opts = opts{.format = STENCIL}, class Template, class T, resizable Buffer> |
44 | | [[nodiscard]] error_ctx stencil(Template&& layout, T&& value, Buffer& buffer) |
45 | | { |
46 | | context ctx{}; |
47 | | |
48 | | if (layout.empty()) [[unlikely]] { |
49 | | ctx.error = error_code::no_read_input; |
50 | | return {ctx.error, ctx.custom_error_message, 0}; |
51 | | } |
52 | | |
53 | | auto p = read_iterators<Opts, false>(layout); |
54 | | auto it = p.first; |
55 | | auto end = p.second; |
56 | | auto outer_start = it; |
57 | | |
58 | | if (not bool(ctx.error)) [[likely]] { |
59 | | auto skip_whitespace = [&] { |
60 | | while (it < end && whitespace_table[uint8_t(*it)]) { |
61 | | ++it; |
62 | | } |
63 | | }; |
64 | | |
65 | | while (it < end) { |
66 | | if (*it == '{') { |
67 | | ++it; |
68 | | if (it != end && *it == '{') { |
69 | | ++it; |
70 | | |
71 | | // Check for triple braces (unescaped HTML) |
72 | | bool is_triple_brace = false; |
73 | | if (it != end && *it == '{') { |
74 | | ++it; |
75 | | is_triple_brace = true; |
76 | | } |
77 | | |
78 | | bool is_section = false; |
79 | | bool is_inverted_section = false; |
80 | | bool is_comment = false; |
81 | | |
82 | | if (it != end && !is_triple_brace) { |
83 | | if (*it == '!') { |
84 | | ++it; |
85 | | is_comment = true; |
86 | | } |
87 | | else if (*it == '#') { |
88 | | ++it; |
89 | | is_section = true; |
90 | | } |
91 | | else if (*it == '^') { |
92 | | ++it; |
93 | | is_inverted_section = true; |
94 | | } |
95 | | } |
96 | | |
97 | | skip_whitespace(); |
98 | | |
99 | | auto start = it; |
100 | | while (it != end && *it != '}' && *it != ' ' && *it != '\t') { |
101 | | ++it; |
102 | | } |
103 | | |
104 | | if (it == end) { |
105 | | ctx.error = error_code::unexpected_end; |
106 | | return {ctx.error, ctx.custom_error_message, size_t(it - outer_start)}; |
107 | | } |
108 | | |
109 | | const sv key{start, size_t(it - start)}; |
110 | | |
111 | | skip_whitespace(); |
112 | | |
113 | | if (is_comment) { |
114 | | while (it < end && !(it + 1 < end && *it == '}' && *(it + 1) == '}')) { |
115 | | ++it; |
116 | | } |
117 | | if (it + 1 < end) { |
118 | | it += 2; // Skip '}}' |
119 | | } |
120 | | continue; |
121 | | } |
122 | | |
123 | | if (is_section || is_inverted_section) { |
124 | | // Find the closing tag '{{/key}}' |
125 | | std::string closing_tag = "{{/" + std::string(key) + "}}"; |
126 | | auto closing_pos = std::search(it, end, closing_tag.begin(), closing_tag.end()); |
127 | | |
128 | | if (closing_pos == end) { |
129 | | ctx.error = error_code::unexpected_end; |
130 | | return {ctx.error, "Closing tag not found for section", size_t(it - outer_start)}; |
131 | | } |
132 | | |
133 | | if (it + 1 < end) { |
134 | | it += 2; // Skip '}}' |
135 | | } |
136 | | |
137 | | // Extract inner template between current position and closing tag |
138 | | std::string_view inner_template(it, closing_pos); |
139 | | it = closing_pos + closing_tag.size(); |
140 | | |
141 | | // Retrieve the value associated with 'key' |
142 | | bool condition = false; |
143 | | bool is_container = false; |
144 | | |
145 | | { |
146 | | static constexpr auto N = reflect<T>::size; |
147 | | static constexpr auto HashInfo = hash_info<T>; |
148 | | |
149 | | const auto index = |
150 | | decode_hash_with_size<STENCIL, T, HashInfo, HashInfo.type>::op(start, end, key.size()); |
151 | | |
152 | | if (index >= N) { |
153 | | ctx.error = error_code::unknown_key; |
154 | | return {ctx.error, ctx.custom_error_message, size_t(it - outer_start)}; |
155 | | } |
156 | | else { |
157 | | visit<N>( |
158 | | [&]<size_t I>() { |
159 | | static constexpr auto TargetKey = get<I>(reflect<T>::keys); |
160 | | if (TargetKey == key) [[likely]] { |
161 | | using field_type = refl_t<T, I>; |
162 | | |
163 | | if constexpr (bool_t<field_type>) { |
164 | | // Boolean field |
165 | | if constexpr (reflectable<T>) { |
166 | | condition = bool(get_member(value, get<I>(to_tie(value)))); |
167 | | } |
168 | | else if constexpr (glaze_object_t<T>) { |
169 | | condition = bool(get_member(value, get<I>(reflect<T>::values))); |
170 | | } |
171 | | } |
172 | | else if constexpr (writable_array_t<field_type>) { |
173 | | // Container field - check if empty for condition |
174 | | is_container = true; |
175 | | |
176 | | if constexpr (reflectable<T>) { |
177 | | auto& container = get_member(value, get<I>(to_tie(value))); |
178 | | condition = !empty_range(container); |
179 | | |
180 | | // Process container iteration for regular sections |
181 | | if (is_section && condition) { |
182 | | using element_type = std::decay_t<decltype(*std::begin(container))>; |
183 | | if constexpr (reflectable<element_type> || glaze_object_t<element_type>) { |
184 | | for (const auto& item : container) { |
185 | | std::string inner_buffer; |
186 | | auto inner_ec = stencil<Opts>(inner_template, item, inner_buffer); |
187 | | if (inner_ec) { |
188 | | ctx.error = inner_ec.ec; |
189 | | return; |
190 | | } |
191 | | buffer.append(inner_buffer); |
192 | | } |
193 | | } |
194 | | else { |
195 | | // For primitive containers, we can't do recursive stencil |
196 | | // This would require special handling for {{.}} syntax |
197 | | ctx.error = error_code::syntax_error; |
198 | | return; |
199 | | } |
200 | | } |
201 | | } |
202 | | else if constexpr (glaze_object_t<T>) { |
203 | | auto& container = get_member(value, get<I>(reflect<T>::values)); |
204 | | condition = !empty_range(container); |
205 | | |
206 | | // Process container iteration for regular sections |
207 | | if (is_section && condition) { |
208 | | using element_type = std::decay_t<decltype(*std::begin(container))>; |
209 | | if constexpr (reflectable<element_type> || glaze_object_t<element_type>) { |
210 | | for (const auto& item : container) { |
211 | | std::string inner_buffer; |
212 | | auto inner_ec = stencil<Opts>(inner_template, item, inner_buffer); |
213 | | if (inner_ec) { |
214 | | ctx.error = inner_ec.ec; |
215 | | return; |
216 | | } |
217 | | buffer.append(inner_buffer); |
218 | | } |
219 | | } |
220 | | else { |
221 | | // For primitive containers, we can't do recursive stencil |
222 | | // This would require special handling for {{.}} syntax |
223 | | ctx.error = error_code::syntax_error; |
224 | | return; |
225 | | } |
226 | | } |
227 | | } |
228 | | } |
229 | | else { |
230 | | // For other types, default to false for sections |
231 | | condition = false; |
232 | | } |
233 | | } |
234 | | else { |
235 | | ctx.error = error_code::unknown_key; |
236 | | } |
237 | | }, |
238 | | index); |
239 | | } |
240 | | } |
241 | | |
242 | | if (bool(ctx.error)) [[unlikely]] { |
243 | | return {ctx.error, ctx.custom_error_message, size_t(it - outer_start)}; |
244 | | } |
245 | | |
246 | | // Handle inverted sections and boolean sections |
247 | | if (is_inverted_section) { |
248 | | // For inverted sections, show content if condition is false |
249 | | if (!condition) { |
250 | | std::string inner_buffer; |
251 | | auto inner_ec = stencil<Opts>(inner_template, value, inner_buffer); |
252 | | if (inner_ec) { |
253 | | return inner_ec; |
254 | | } |
255 | | buffer.append(inner_buffer); |
256 | | } |
257 | | } |
258 | | else if (is_section && !is_container) { |
259 | | // For boolean sections (non-containers), show content if condition is true |
260 | | if (condition) { |
261 | | std::string inner_buffer; |
262 | | auto inner_ec = stencil<Opts>(inner_template, value, inner_buffer); |
263 | | if (inner_ec) { |
264 | | return inner_ec; |
265 | | } |
266 | | buffer.append(inner_buffer); |
267 | | } |
268 | | } |
269 | | // Container iteration for regular sections was already handled above |
270 | | |
271 | | skip_whitespace(); |
272 | | continue; |
273 | | } |
274 | | |
275 | | // Handle regular placeholder (double braces) or unescaped (triple braces) |
276 | | static constexpr auto N = reflect<T>::size; |
277 | | static constexpr auto HashInfo = hash_info<T>; |
278 | | |
279 | | const auto index = |
280 | | decode_hash_with_size<STENCIL, T, HashInfo, HashInfo.type>::op(start, end, key.size()); |
281 | | |
282 | | if (index >= N) [[unlikely]] { |
283 | | ctx.error = error_code::unknown_key; |
284 | | return {ctx.error, ctx.custom_error_message, size_t(it - outer_start)}; |
285 | | } |
286 | | else [[likely]] { |
287 | | // For triple braces, we need to expect three closing braces |
288 | | size_t expected_closing_braces = is_triple_brace ? 3 : 2; |
289 | | |
290 | | // Check for correct closing braces |
291 | | size_t closing_brace_count = 0; |
292 | | auto temp_it = it; |
293 | | while (temp_it < end && *temp_it == '}' && closing_brace_count < 3) { |
294 | | ++temp_it; |
295 | | ++closing_brace_count; |
296 | | } |
297 | | |
298 | | if (closing_brace_count < expected_closing_braces) { |
299 | | ctx.error = error_code::syntax_error; |
300 | | return {ctx.error, ctx.custom_error_message, size_t(it - outer_start)}; |
301 | | } |
302 | | |
303 | | // Serialize the value |
304 | | std::string temp_buffer; |
305 | | static constexpr auto RawOpts = |
306 | | set_json<opt_true<Opts, &opts::raw>>(); // write out string like values without quotes |
307 | | |
308 | | visit<N>( |
309 | | [&]<size_t I>() { |
310 | | static constexpr auto TargetKey = get<I>(reflect<T>::keys); |
311 | | if ((TargetKey.size() == key.size()) && comparitor<TargetKey>(start)) [[likely]] { |
312 | | size_t ix = 0; |
313 | | temp_buffer.resize(2 * write_padding_bytes); |
314 | | |
315 | | if constexpr (reflectable<T>) { |
316 | | serialize<JSON>::template op<RawOpts>(get_member(value, get<I>(to_tie(value))), ctx, |
317 | | temp_buffer, ix); |
318 | | } |
319 | | else if constexpr (glaze_object_t<T>) { |
320 | | serialize<JSON>::template op<RawOpts>(get_member(value, get<I>(reflect<T>::values)), |
321 | | ctx, temp_buffer, ix); |
322 | | } |
323 | | |
324 | | temp_buffer.resize(ix); |
325 | | } |
326 | | else { |
327 | | ctx.error = error_code::unknown_key; |
328 | | } |
329 | | }, |
330 | | index); |
331 | | |
332 | | if (bool(ctx.error)) [[unlikely]] { |
333 | | return {ctx.error, ctx.custom_error_message, size_t(it - outer_start)}; |
334 | | } |
335 | | |
336 | | // Apply HTML escaping for double braces, leave unescaped for triple braces |
337 | | if (is_triple_brace) { |
338 | | buffer.append(temp_buffer); |
339 | | } |
340 | | else { |
341 | | if constexpr (Opts.format == MUSTACHE) { |
342 | | buffer.append(html_escape(temp_buffer)); |
343 | | } |
344 | | else { |
345 | | buffer.append(temp_buffer); |
346 | | } |
347 | | } |
348 | | |
349 | | // Skip the closing braces |
350 | | it += expected_closing_braces; |
351 | | continue; |
352 | | } |
353 | | } |
354 | | else { |
355 | | buffer.append("{"); |
356 | | // 'it' is already incremented past the first '{' |
357 | | } |
358 | | } |
359 | | else { |
360 | | buffer.push_back(*it); |
361 | | ++it; |
362 | | } |
363 | | } |
364 | | } |
365 | | |
366 | | if (bool(ctx.error)) [[unlikely]] { |
367 | | return {ctx.error, ctx.custom_error_message, size_t(it - outer_start)}; |
368 | | } |
369 | | |
370 | | return {}; |
371 | | } |
372 | | |
373 | | template <auto Opts = opts{.format = STENCIL}, class Template, class T> |
374 | | [[nodiscard]] expected<std::string, error_ctx> stencil(Template&& layout, T&& value) |
375 | | { |
376 | | std::string buffer{}; |
377 | | auto ec = stencil<Opts>(std::forward<Template>(layout), std::forward<T>(value), buffer); |
378 | | if (ec) { |
379 | | return unexpected<error_ctx>(ec); |
380 | | } |
381 | | return {buffer}; |
382 | | } |
383 | | |
384 | | template <auto Opts = opts{.format = MUSTACHE}, class Template, class T, resizable Buffer> |
385 | | requires(Opts.format == MUSTACHE) |
386 | | [[nodiscard]] error_ctx mustache(Template&& layout, T&& value, Buffer& buffer) |
387 | | { |
388 | | return stencil<Opts>(std::forward<Template>(layout), std::forward<T>(value), buffer); |
389 | | } |
390 | | |
391 | | template <auto Opts = opts{.format = MUSTACHE}, class Template, class T> |
392 | | requires(Opts.format == MUSTACHE) |
393 | | [[nodiscard]] expected<std::string, error_ctx> mustache(Template&& layout, T&& value) |
394 | | { |
395 | | std::string buffer{}; |
396 | | auto ec = stencil<Opts>(std::forward<Template>(layout), std::forward<T>(value), buffer); |
397 | | if (ec) { |
398 | | return unexpected<error_ctx>(ec); |
399 | | } |
400 | | return {buffer}; |
401 | | } |
402 | | } |