| |
1 /* |
| |
2 * Purple - Internet Messaging Library |
| |
3 * Copyright (C) Pidgin Developers <devel@pidgin.im> |
| |
4 * |
| |
5 * Purple is the legal property of its developers, whose names are too numerous |
| |
6 * to list here. Please refer to the COPYRIGHT file distributed with this |
| |
7 * source distribution. |
| |
8 * |
| |
9 * This program is free software; you can redistribute it and/or modify |
| |
10 * it under the terms of the GNU General Public License as published by |
| |
11 * the Free Software Foundation; either version 2 of the License, or |
| |
12 * (at your option) any later version. |
| |
13 * |
| |
14 * This program is distributed in the hope that it will be useful, |
| |
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| |
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| |
17 * GNU General Public License for more details. |
| |
18 * |
| |
19 * You should have received a copy of the GNU General Public License |
| |
20 * along with this program; if not, see <https://www.gnu.org/licenses/>. |
| |
21 */ |
| |
22 |
| |
23 #include <ctype.h> |
| |
24 |
| |
25 #include <purple.h> |
| |
26 |
| |
27 #include "zephyr_html.h" |
| |
28 |
| |
29 typedef struct _zframe zframe; |
| |
30 |
| |
31 struct _zframe { |
| |
32 /* common part */ |
| |
33 /* true for everything but @color, since inside the parens of that one is |
| |
34 * the color. */ |
| |
35 gboolean has_closer; |
| |
36 /* </i>, </font>, </b>, etc. */ |
| |
37 const char *closing; |
| |
38 /* text including the opening html thingie. */ |
| |
39 GString *text; |
| |
40 |
| |
41 /* html_to_zephyr */ |
| |
42 /* @i, @b, etc. */ |
| |
43 const char *env; |
| |
44 /* }=1, ]=2, )=4, >=8 */ |
| |
45 int closer_mask; |
| |
46 /* href for links */ |
| |
47 gboolean is_href; |
| |
48 GString *href; |
| |
49 |
| |
50 /* zephyr_to_html */ |
| |
51 /* }, ], ), > */ |
| |
52 char *closer; |
| |
53 }; |
| |
54 |
| |
55 static zframe * |
| |
56 zframe_new_with_text(const gchar *text, const gchar *closing, gboolean has_closer) |
| |
57 { |
| |
58 zframe *frame = g_new(zframe, 1); |
| |
59 |
| |
60 frame->text = g_string_new(text); |
| |
61 frame->closing = closing; |
| |
62 frame->has_closer = has_closer; |
| |
63 |
| |
64 return frame; |
| |
65 } |
| |
66 |
| |
67 static inline zframe * |
| |
68 zframe_new(const gchar *closing, gboolean has_closer) |
| |
69 { |
| |
70 return zframe_new_with_text("", closing, has_closer); |
| |
71 } |
| |
72 |
| |
73 static gboolean |
| |
74 zframe_href_has_prefix(const zframe *frame, const gchar *prefix) |
| |
75 { |
| |
76 gsize prefix_len = strlen(prefix); |
| |
77 |
| |
78 return (frame->href->len == (prefix_len + frame->text->len)) && |
| |
79 !strncmp(frame->href->str, prefix, prefix_len) && |
| |
80 purple_strequal(frame->href->str + prefix_len, frame->text->str); |
| |
81 } |
| |
82 |
| |
83 static gsize |
| |
84 html_to_zephyr_pop(GQueue *frames) |
| |
85 { |
| |
86 zframe *popped = (zframe *)g_queue_pop_head(frames); |
| |
87 zframe *head = (zframe *)g_queue_peek_head(frames); |
| |
88 gsize result = strlen(popped->closing); |
| |
89 |
| |
90 if (popped->is_href) { |
| |
91 head->href = popped->text; |
| |
92 } else { |
| |
93 g_string_append(head->text, popped->env); |
| |
94 if (popped->has_closer) { |
| |
95 g_string_append_c(head->text, |
| |
96 (popped->closer_mask & 1) ? '{' : |
| |
97 (popped->closer_mask & 2) ? '[' : |
| |
98 (popped->closer_mask & 4) ? '(' : |
| |
99 '<'); |
| |
100 } |
| |
101 g_string_append(head->text, popped->text->str); |
| |
102 if (popped->href) { |
| |
103 if (!purple_strequal(popped->href->str, popped->text->str) && |
| |
104 !zframe_href_has_prefix(popped, "http://") && |
| |
105 !zframe_href_has_prefix(popped, "mailto:")) { |
| |
106 g_string_append(head->text, " <"); |
| |
107 g_string_append(head->text, popped->href->str); |
| |
108 if (popped->closer_mask & ~8) { |
| |
109 g_string_append_c(head->text, '>'); |
| |
110 popped->closer_mask &= ~8; |
| |
111 } else { |
| |
112 g_string_append(head->text, "@{>}"); |
| |
113 } |
| |
114 } |
| |
115 g_string_free(popped->href, TRUE); |
| |
116 } |
| |
117 if (popped->has_closer) { |
| |
118 g_string_append_c(head->text, |
| |
119 (popped->closer_mask & 1) ? '}' : |
| |
120 (popped->closer_mask & 2) ? ']' : |
| |
121 (popped->closer_mask & 4) ? ')' : |
| |
122 '>'); |
| |
123 } |
| |
124 if (!popped->has_closer) { |
| |
125 head->closer_mask = popped->closer_mask; |
| |
126 } |
| |
127 g_string_free(popped->text, TRUE); |
| |
128 } |
| |
129 g_free(popped); |
| |
130 return result; |
| |
131 } |
| |
132 |
| |
133 char * |
| |
134 html_to_zephyr(const char *message) |
| |
135 { |
| |
136 GQueue frames = G_QUEUE_INIT; |
| |
137 zframe *frame, *new_f; |
| |
138 char *ret; |
| |
139 |
| |
140 if (*message == '\0') |
| |
141 return g_strdup(""); |
| |
142 |
| |
143 frame = zframe_new(NULL, FALSE); |
| |
144 frame->href = NULL; |
| |
145 frame->is_href = FALSE; |
| |
146 frame->env = ""; |
| |
147 frame->closer_mask = 15; |
| |
148 |
| |
149 g_queue_push_head(&frames, frame); |
| |
150 |
| |
151 purple_debug_info("zephyr", "html received %s\n", message); |
| |
152 while (*message) { |
| |
153 frame = (zframe *)g_queue_peek_head(&frames); |
| |
154 if (frame->closing && purple_str_has_caseprefix(message, frame->closing)) { |
| |
155 message += html_to_zephyr_pop(&frames); |
| |
156 } else if (*message == '<') { |
| |
157 if (!g_ascii_strncasecmp(message + 1, "i>", 2)) { |
| |
158 new_f = zframe_new("</i>", TRUE); |
| |
159 new_f->href = NULL; |
| |
160 new_f->is_href = FALSE; |
| |
161 new_f->env = "@i"; |
| |
162 new_f->closer_mask = 15; |
| |
163 g_queue_push_head(&frames, new_f); |
| |
164 message += 3; |
| |
165 } else if (!g_ascii_strncasecmp(message + 1, "b>", 2)) { |
| |
166 new_f = zframe_new("</b>", TRUE); |
| |
167 new_f->href = NULL; |
| |
168 new_f->is_href = FALSE; |
| |
169 new_f->env = "@b"; |
| |
170 new_f->closer_mask = 15; |
| |
171 g_queue_push_head(&frames, new_f); |
| |
172 message += 3; |
| |
173 } else if (!g_ascii_strncasecmp(message + 1, "br>", 3)) { |
| |
174 g_string_append_c(frame->text, '\n'); |
| |
175 message += 4; |
| |
176 } else if (!g_ascii_strncasecmp(message + 1, "a href=\"", 8)) { |
| |
177 message += 9; |
| |
178 new_f = zframe_new("</a>", FALSE); |
| |
179 new_f->href = NULL; |
| |
180 new_f->is_href = FALSE; |
| |
181 new_f->env = ""; |
| |
182 new_f->closer_mask = frame->closer_mask; |
| |
183 g_queue_push_head(&frames, new_f); |
| |
184 new_f = zframe_new("\">", FALSE); |
| |
185 new_f->href = NULL; |
| |
186 new_f->is_href = TRUE; |
| |
187 new_f->closer_mask = frame->closer_mask; |
| |
188 g_queue_push_head(&frames, new_f); |
| |
189 } else if (!g_ascii_strncasecmp(message + 1, "font", 4)) { |
| |
190 new_f = zframe_new("</font>", TRUE); |
| |
191 new_f->href = NULL; |
| |
192 new_f->is_href = FALSE; |
| |
193 new_f->closer_mask = 15; |
| |
194 g_queue_push_head(&frames, new_f); |
| |
195 message += 5; |
| |
196 while (*message == ' ') { |
| |
197 message++; |
| |
198 } |
| |
199 if (!g_ascii_strncasecmp(message, "color=\"", 7)) { |
| |
200 message += 7; |
| |
201 new_f->env = "@"; |
| |
202 new_f = zframe_new("\">", TRUE); |
| |
203 new_f->env = "@color"; |
| |
204 new_f->href = NULL; |
| |
205 new_f->is_href = FALSE; |
| |
206 new_f->closer_mask = 15; |
| |
207 g_queue_push_head(&frames, new_f); |
| |
208 } else if (!g_ascii_strncasecmp(message, "face=\"", 6)) { |
| |
209 message += 6; |
| |
210 new_f->env = "@"; |
| |
211 new_f = zframe_new("\">", TRUE); |
| |
212 new_f->env = "@font"; |
| |
213 new_f->href = NULL; |
| |
214 new_f->is_href = FALSE; |
| |
215 new_f->closer_mask = 15; |
| |
216 g_queue_push_head(&frames, new_f); |
| |
217 } else if (!g_ascii_strncasecmp(message, "size=\"", 6)) { |
| |
218 message += 6; |
| |
219 if ((*message == '1') || (*message == '2')) { |
| |
220 new_f->env = "@small"; |
| |
221 } else if ((*message == '3') || (*message == '4')) { |
| |
222 new_f->env = "@medium"; |
| |
223 } else if ((*message == '5') || (*message == '6') |
| |
224 || (*message == '7')) { |
| |
225 new_f->env = "@large"; |
| |
226 } else { |
| |
227 new_f->env = ""; |
| |
228 new_f->has_closer = FALSE; |
| |
229 new_f->closer_mask = frame->closer_mask; |
| |
230 } |
| |
231 message += 3; |
| |
232 } else { |
| |
233 /* Drop all unrecognized/misparsed font tags */ |
| |
234 new_f->env = ""; |
| |
235 new_f->has_closer = FALSE; |
| |
236 new_f->closer_mask = frame->closer_mask; |
| |
237 while (g_ascii_strncasecmp(message, "\">", 2) != 0) { |
| |
238 message++; |
| |
239 } |
| |
240 if (*message != '\0') { |
| |
241 message += 2; |
| |
242 } |
| |
243 } |
| |
244 } else { |
| |
245 /* Catch all for all unrecognized/misparsed <foo> tage */ |
| |
246 g_string_append_c(frame->text, *message++); |
| |
247 } |
| |
248 } else if (*message == '@') { |
| |
249 g_string_append(frame->text, "@@"); |
| |
250 message++; |
| |
251 } else if (*message == '}') { |
| |
252 if (frame->closer_mask & ~1) { |
| |
253 frame->closer_mask &= ~1; |
| |
254 g_string_append_c(frame->text, *message++); |
| |
255 } else { |
| |
256 g_string_append(frame->text, "@[}]"); |
| |
257 message++; |
| |
258 } |
| |
259 } else if (*message == ']') { |
| |
260 if (frame->closer_mask & ~2) { |
| |
261 frame->closer_mask &= ~2; |
| |
262 g_string_append_c(frame->text, *message++); |
| |
263 } else { |
| |
264 g_string_append(frame->text, "@{]}"); |
| |
265 message++; |
| |
266 } |
| |
267 } else if (*message == ')') { |
| |
268 if (frame->closer_mask & ~4) { |
| |
269 frame->closer_mask &= ~4; |
| |
270 g_string_append_c(frame->text, *message++); |
| |
271 } else { |
| |
272 g_string_append(frame->text, "@{)}"); |
| |
273 message++; |
| |
274 } |
| |
275 } else if (!g_ascii_strncasecmp(message, ">", 4)) { |
| |
276 if (frame->closer_mask & ~8) { |
| |
277 frame->closer_mask &= ~8; |
| |
278 g_string_append_c(frame->text, *message++); |
| |
279 } else { |
| |
280 g_string_append(frame->text, "@{>}"); |
| |
281 message += 4; |
| |
282 } |
| |
283 } else { |
| |
284 g_string_append_c(frame->text, *message++); |
| |
285 } |
| |
286 } |
| |
287 frame = (zframe *)g_queue_pop_head(&frames); |
| |
288 ret = g_string_free(frame->text, FALSE); |
| |
289 g_free(frame); |
| |
290 purple_debug_info("zephyr", "zephyr outputted %s\n", ret); |
| |
291 return ret; |
| |
292 } |
| |
293 |
| |
294 static void |
| |
295 zephyr_to_html_pop(GQueue *frames, gboolean *last_had_closer) |
| |
296 { |
| |
297 zframe *popped = (zframe *)g_queue_pop_head(frames); |
| |
298 zframe *head = (zframe *)g_queue_peek_head(frames); |
| |
299 |
| |
300 g_string_append(head->text, popped->text->str); |
| |
301 g_string_append(head->text, popped->closing); |
| |
302 |
| |
303 if (last_had_closer != NULL) { |
| |
304 *last_had_closer = popped->has_closer; |
| |
305 } |
| |
306 |
| |
307 g_string_free(popped->text, TRUE); |
| |
308 g_free(popped); |
| |
309 } |
| |
310 |
| |
311 char * |
| |
312 zephyr_to_html(const char *message) |
| |
313 { |
| |
314 GQueue frames = G_QUEUE_INIT; |
| |
315 zframe *frame; |
| |
316 char *ret; |
| |
317 |
| |
318 frame = zframe_new("", FALSE); |
| |
319 frame->closer = NULL; |
| |
320 |
| |
321 g_queue_push_head(&frames, frame); |
| |
322 |
| |
323 while (*message) { |
| |
324 frame = (zframe *)g_queue_peek_head(&frames); |
| |
325 if (*message == '@' && message[1] == '@') { |
| |
326 g_string_append(frame->text, "@"); |
| |
327 message += 2; |
| |
328 } else if (*message == '@') { |
| |
329 int end = 1; |
| |
330 while (message[end] && (isalnum(message[end]) || message[end] == '_')) { |
| |
331 end++; |
| |
332 } |
| |
333 if (message[end] && |
| |
334 (message[end] == '{' || message[end] == '[' || message[end] == '(' || |
| |
335 !g_ascii_strncasecmp(message + end, "<", 4))) { |
| |
336 zframe *new_f; |
| |
337 char *buf; |
| |
338 char *closer; |
| |
339 buf = g_new0(char, end); |
| |
340 g_snprintf(buf, end, "%s", message + 1); |
| |
341 message += end; |
| |
342 closer = (*message == '{' ? "}" : |
| |
343 *message == '[' ? "]" : |
| |
344 *message == '(' ? ")" : |
| |
345 ">"); |
| |
346 message += (*message == '&' ? 4 : 1); |
| |
347 if (!g_ascii_strcasecmp(buf, "italic") || !g_ascii_strcasecmp(buf, "i")) { |
| |
348 new_f = zframe_new_with_text("<i>", "</i>", TRUE); |
| |
349 } else if (!g_ascii_strcasecmp(buf, "small")) { |
| |
350 new_f = zframe_new_with_text("<font size=\"1\">", "</font>", TRUE); |
| |
351 } else if (!g_ascii_strcasecmp(buf, "medium")) { |
| |
352 new_f = zframe_new_with_text("<font size=\"3\">", "</font>", TRUE); |
| |
353 } else if (!g_ascii_strcasecmp(buf, "large")) { |
| |
354 new_f = zframe_new_with_text("<font size=\"7\">", "</font>", TRUE); |
| |
355 } else if (!g_ascii_strcasecmp(buf, "bold") |
| |
356 || !g_ascii_strcasecmp(buf, "b")) { |
| |
357 new_f = zframe_new_with_text("<b>", "</b>", TRUE); |
| |
358 } else if (!g_ascii_strcasecmp(buf, "font")) { |
| |
359 zframe *extra_f; |
| |
360 extra_f = zframe_new("</font>", FALSE); |
| |
361 extra_f->closer = frame->closer; |
| |
362 g_queue_push_head(&frames, extra_f); |
| |
363 new_f = zframe_new_with_text("<font face=\"", "\">", TRUE); |
| |
364 } else if (!g_ascii_strcasecmp(buf, "color")) { |
| |
365 zframe *extra_f; |
| |
366 extra_f = zframe_new("</font>", FALSE); |
| |
367 extra_f->closer = frame->closer; |
| |
368 g_queue_push_head(&frames, extra_f); |
| |
369 new_f = zframe_new_with_text("<font color=\"", "\">", TRUE); |
| |
370 } else { |
| |
371 new_f = zframe_new("", TRUE); |
| |
372 } |
| |
373 new_f->closer = closer; |
| |
374 g_queue_push_head(&frames, new_f); |
| |
375 g_free(buf); |
| |
376 } else { |
| |
377 /* Not a formatting tag, add the character as normal. */ |
| |
378 g_string_append_c(frame->text, *message++); |
| |
379 } |
| |
380 } else if (frame->closer && purple_str_has_caseprefix(message, frame->closer)) { |
| |
381 message += strlen(frame->closer); |
| |
382 if (g_queue_get_length(&frames) > 1) { |
| |
383 gboolean last_had_closer; |
| |
384 |
| |
385 do { |
| |
386 zephyr_to_html_pop(&frames, &last_had_closer); |
| |
387 } while (g_queue_get_length(&frames) > 1 && !last_had_closer); |
| |
388 } else { |
| |
389 g_string_append_c(frame->text, *message); |
| |
390 } |
| |
391 } else if (*message == '\n') { |
| |
392 g_string_append(frame->text, "<br>"); |
| |
393 message++; |
| |
394 } else { |
| |
395 g_string_append_c(frame->text, *message++); |
| |
396 } |
| |
397 } |
| |
398 /* go through all the stuff that they didn't close */ |
| |
399 while (g_queue_get_length(&frames) > 1) { |
| |
400 zephyr_to_html_pop(&frames, NULL); |
| |
401 } |
| |
402 frame = (zframe *)g_queue_pop_head(&frames); |
| |
403 ret = g_string_free(frame->text, FALSE); |
| |
404 g_free(frame); |
| |
405 return ret; |
| |
406 } |