1 /* Output stream that produces HTML output.
2 Copyright (C) 2006-2009, 2019 Free Software Foundation, Inc.
3 Written by Bruno Haible <bruno@clisp.org>, 2006.
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>. */
17
18 #include <config.h>
19
20 /* Specification. */
21 #include "html-ostream.h"
22
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26
27 #include "gl_xlist.h"
28 #include "gl_array_list.h"
29 #include "minmax.h"
30 #include "unistr.h"
31 #include "xalloc.h"
32
33 struct html_ostream : struct ostream
34 {
35 fields:
36 /* The destination stream. */
37 ostream_t destination;
38 /* The current hyperlink ref. */
39 char *hyperlink_ref;
40 /* The stack of active CSS classes. */
41 gl_list_t /* <char *> */ class_stack;
42 /* Current and last size of the active portion of this stack. Always
43 size(class_stack) == max(curr_class_stack_size,last_class_stack_size). */
44 size_t curr_class_stack_size;
45 size_t last_class_stack_size;
46 /* Last few bytes that could not yet be converted. */
47 #define BUFSIZE 6
48 char buf[BUFSIZE];
49 size_t buflen;
50 };
51
52 /* Emit an HTML attribute value.
53 quote is either '"' or '\''. */
54 static void
write_attribute_value(html_ostream_t stream,const char * value,char quote)55 write_attribute_value (html_ostream_t stream, const char *value, char quote)
56 {
57 /* Need to escape the '<', '>', '&', quote characters. */
58 ostream_t destination = stream->destination;
59 const char *p = value;
60
61 for (;;)
62 {
63 const char *q = p;
64
65 while (*q != '\0' && *q != '<' && *q != '>' && *q != '&' && *q != quote)
66 q++;
67 if (p < q)
68 ostream_write_mem (destination, p, q - p);
69 if (*q == '\0')
70 break;
71 switch (*q)
72 {
73 case '<':
74 ostream_write_str (destination, "<");
75 break;
76 case '>':
77 ostream_write_str (destination, ">");
78 break;
79 case '&':
80 ostream_write_str (destination, "&");
81 break;
82 case '"':
83 ostream_write_str (destination, """);
84 break;
85 case '\'':
86 ostream_write_str (destination, "'");
87 break;
88 default:
89 abort ();
90 }
91 p = q + 1;
92 }
93 }
94
95 /* Implementation of ostream_t methods. */
96
97 static void
verify_invariants(html_ostream_t stream)98 verify_invariants (html_ostream_t stream)
99 {
100 /* Verify the invariant regarding size(class_stack). */
101 if (gl_list_size (stream->class_stack)
102 != MAX (stream->curr_class_stack_size, stream->last_class_stack_size))
103 abort ();
104 }
105
106 /* Removes the excess elements of class_stack.
107 Needs to be called after max(curr_class_stack_size,last_class_stack_size)
108 may have been reduced. */
109 static void
shrink_class_stack(html_ostream_t stream)110 shrink_class_stack (html_ostream_t stream)
111 {
112 size_t keep =
113 MAX (stream->curr_class_stack_size, stream->last_class_stack_size);
114 size_t i = gl_list_size (stream->class_stack);
115 while (i > keep)
116 {
117 i--;
118 free ((char *) gl_list_get_at (stream->class_stack, i));
119 gl_list_remove_at (stream->class_stack, i);
120 }
121 }
122
123 /* Emits <span> or </span> tags, to follow the increase / decrease of the
124 class_stack from last_class_stack_size to curr_class_stack_size.
125 When done, sets last_class_stack_size to curr_class_stack_size. */
126 static void
emit_pending_spans(html_ostream_t stream,bool shrink_stack)127 emit_pending_spans (html_ostream_t stream, bool shrink_stack)
128 {
129 if (stream->curr_class_stack_size > stream->last_class_stack_size)
130 {
131 size_t i;
132
133 for (i = stream->last_class_stack_size; i < stream->curr_class_stack_size; i++)
134 {
135 char *classname = (char *) gl_list_get_at (stream->class_stack, i);
136
137 ostream_write_str (stream->destination, "<span class=\"");
138 ostream_write_str (stream->destination, classname);
139 ostream_write_str (stream->destination, "\">");
140 }
141 stream->last_class_stack_size = stream->curr_class_stack_size;
142 }
143 else if (stream->curr_class_stack_size < stream->last_class_stack_size)
144 {
145 size_t i;
146
147 for (i = stream->last_class_stack_size; i > stream->curr_class_stack_size; i--)
148 ostream_write_str (stream->destination, "</span>");
149 stream->last_class_stack_size = stream->curr_class_stack_size;
150 if (shrink_stack)
151 shrink_class_stack (stream);
152 }
153 /* Here last_class_stack_size == curr_class_stack_size. */
154 if (shrink_stack)
155 verify_invariants (stream);
156 }
157
158 static void
write_mem(html_ostream_t stream,const void * data,size_t len)159 html_ostream::write_mem (html_ostream_t stream, const void *data, size_t len)
160 {
161 if (len > 0)
162 {
163 #define BUFFERSIZE 2048
164 char inbuffer[BUFFERSIZE];
165 size_t inbufcount;
166
167 inbufcount = stream->buflen;
168 if (inbufcount > 0)
169 memcpy (inbuffer, stream->buf, inbufcount);
170 for (;;)
171 {
172 /* At this point, inbuffer[0..inbufcount-1] is filled. */
173 {
174 /* Combine the previous rest with a chunk of new input. */
175 size_t n =
176 (len <= BUFFERSIZE - inbufcount ? len : BUFFERSIZE - inbufcount);
177
178 if (n > 0)
179 {
180 memcpy (inbuffer + inbufcount, data, n);
181 data = (char *) data + n;
182 inbufcount += n;
183 len -= n;
184 }
185 }
186 {
187 /* Handle complete UTF-8 characters. */
188 const char *inptr = inbuffer;
189 size_t insize = inbufcount;
190
191 while (insize > 0)
192 {
193 unsigned char c0;
194 ucs4_t uc;
195 int nbytes;
196
197 c0 = ((const unsigned char *) inptr)[0];
198 if (insize < (c0 < 0xc0 ? 1 : c0 < 0xe0 ? 2 : c0 < 0xf0 ? 3 :
199 c0 < 0xf8 ? 4 : c0 < 0xfc ? 5 : 6))
200 break;
201
202 nbytes = u8_mbtouc (&uc, (const unsigned char *) inptr, insize);
203
204 if (uc == '\n')
205 {
206 verify_invariants (stream);
207 /* Emit </span> tags to follow the decrease of the class stack
208 from last_class_stack_size to 0. Then emit the newline.
209 Then prepare for emitting <span> tags to go back from 0
210 to curr_class_stack_size. */
211 size_t prev_class_stack_size = stream->curr_class_stack_size;
212 stream->curr_class_stack_size = 0;
213 emit_pending_spans (stream, false);
214 stream->curr_class_stack_size = prev_class_stack_size;
215 ostream_write_str (stream->destination, "<br/>");
216 shrink_class_stack (stream);
217 verify_invariants (stream);
218 }
219 else
220 {
221 emit_pending_spans (stream, true);
222
223 switch (uc)
224 {
225 case '"':
226 ostream_write_str (stream->destination, """);
227 break;
228 case '&':
229 ostream_write_str (stream->destination, "&");
230 break;
231 case '<':
232 ostream_write_str (stream->destination, "<");
233 break;
234 case '>':
235 /* Needed to avoid "]]>" in the output. */
236 ostream_write_str (stream->destination, ">");
237 break;
238 case ' ':
239 /* Needed because HTML viewers merge adjacent spaces
240 and drop spaces adjacent to <br> and similar. */
241 ostream_write_str (stream->destination, " ");
242 break;
243 default:
244 if (uc >= 0x20 && uc < 0x7F)
245 {
246 /* Output ASCII characters as such. */
247 char bytes[1];
248 bytes[0] = uc;
249 ostream_write_mem (stream->destination, bytes, 1);
250 }
251 else
252 {
253 /* Output non-ASCII characters in #&nnn;
254 notation. */
255 char bytes[32];
256 sprintf (bytes, "&#%d;", (int) uc);
257 ostream_write_str (stream->destination, bytes);
258 }
259 break;
260 }
261 }
262
263 inptr += nbytes;
264 insize -= nbytes;
265 }
266 /* Put back the unconverted part. */
267 if (insize > BUFSIZE)
268 abort ();
269 if (len == 0)
270 {
271 if (insize > 0)
272 memcpy (stream->buf, inptr, insize);
273 stream->buflen = insize;
274 break;
275 }
276 if (insize > 0)
277 memmove (inbuffer, inptr, insize);
278 inbufcount = insize;
279 }
280 }
281 #undef BUFFERSIZE
282 }
283 }
284
285 static void
flush(html_ostream_t stream,ostream_flush_scope_t scope)286 html_ostream::flush (html_ostream_t stream, ostream_flush_scope_t scope)
287 {
288 verify_invariants (stream);
289 /* stream->buf[] contains only a few bytes that don't correspond to a
290 character. Can't flush it. */
291 /* Close the open <span> tags, and prepare for reopening the same <span>
292 tags. */
293 size_t prev_class_stack_size = stream->curr_class_stack_size;
294 stream->curr_class_stack_size = 0;
295 emit_pending_spans (stream, false);
296 stream->curr_class_stack_size = prev_class_stack_size;
297 shrink_class_stack (stream);
298 verify_invariants (stream);
299
300 if (scope != FLUSH_THIS_STREAM)
301 ostream_flush (stream->destination, scope);
302 }
303
304 static void
free(html_ostream_t stream)305 html_ostream::free (html_ostream_t stream)
306 {
307 stream->curr_class_stack_size = 0;
308 emit_pending_spans (stream, true);
309 if (stream->hyperlink_ref != NULL)
310 {
311 /* Close the current <a> element. */
312 ostream_write_str (stream->destination, "</a>");
313 free (stream->hyperlink_ref);
314 }
315 verify_invariants (stream);
316 gl_list_free (stream->class_stack);
317 free (stream);
318 }
319
320 /* Implementation of html_ostream_t methods. */
321
322 static void
begin_span(html_ostream_t stream,const char * classname)323 html_ostream::begin_span (html_ostream_t stream, const char *classname)
324 {
325 verify_invariants (stream);
326 if (stream->last_class_stack_size > stream->curr_class_stack_size
327 && strcmp ((char *) gl_list_get_at (stream->class_stack,
328 stream->curr_class_stack_size),
329 classname) != 0)
330 emit_pending_spans (stream, true);
331 /* Now either
332 last_class_stack_size <= curr_class_stack_size
333 - in this case we have to append the given CLASSNAME -
334 or
335 last_class_stack_size > curr_class_stack_size
336 && class_stack[curr_class_stack_size] == CLASSNAME
337 - in this case we only need to increment curr_class_stack_size. */
338 if (stream->last_class_stack_size <= stream->curr_class_stack_size)
339 gl_list_add_at (stream->class_stack, stream->curr_class_stack_size,
340 xstrdup (classname));
341 stream->curr_class_stack_size++;
342 verify_invariants (stream);
343 }
344
345 static void
end_span(html_ostream_t stream,const char * classname)346 html_ostream::end_span (html_ostream_t stream, const char *classname)
347 {
348 verify_invariants (stream);
349 if (stream->curr_class_stack_size > 0)
350 {
351 char *innermost_active_span =
352 (char *) gl_list_get_at (stream->class_stack,
353 stream->curr_class_stack_size - 1);
354 if (strcmp (innermost_active_span, classname) == 0)
355 {
356 stream->curr_class_stack_size--;
357 shrink_class_stack (stream);
358 verify_invariants (stream);
359 return;
360 }
361 }
362 /* Improperly nested begin_span/end_span calls. */
363 abort ();
364 }
365
366 static const char *
get_hyperlink_ref(html_ostream_t stream)367 html_ostream::get_hyperlink_ref (html_ostream_t stream)
368 {
369 return stream->hyperlink_ref;
370 }
371
372 static void
set_hyperlink_ref(html_ostream_t stream,const char * ref)373 html_ostream::set_hyperlink_ref (html_ostream_t stream, const char *ref)
374 {
375 char *ref_copy = (ref != NULL ? xstrdup (ref) : NULL);
376
377 verify_invariants (stream);
378 if (stream->hyperlink_ref != NULL)
379 {
380 /* Close the open <span> tags, and prepare for reopening the same <span>
381 tags. */
382 size_t prev_class_stack_size = stream->curr_class_stack_size;
383 stream->curr_class_stack_size = 0;
384 emit_pending_spans (stream, false);
385 stream->curr_class_stack_size = prev_class_stack_size;
386 /* Close the current <a> element. */
387 ostream_write_str (stream->destination, "</a>");
388 shrink_class_stack (stream);
389
390 free (stream->hyperlink_ref);
391 }
392 stream->hyperlink_ref = ref_copy;
393 if (stream->hyperlink_ref != NULL)
394 {
395 /* Close the open <span> tags, and prepare for reopening the same <span>
396 tags. */
397 size_t prev_class_stack_size = stream->curr_class_stack_size;
398 stream->curr_class_stack_size = 0;
399 emit_pending_spans (stream, false);
400 stream->curr_class_stack_size = prev_class_stack_size;
401 /* Open an <a> element. */
402 ostream_write_str (stream->destination, "<a href=\"");
403 write_attribute_value (stream, stream->hyperlink_ref, '"');
404 ostream_write_str (stream->destination, "\">");
405 shrink_class_stack (stream);
406 }
407 verify_invariants (stream);
408 }
409
410 static void
flush_to_current_style(html_ostream_t stream)411 html_ostream::flush_to_current_style (html_ostream_t stream)
412 {
413 verify_invariants (stream);
414 /* stream->buf[] contains only a few bytes that don't correspond to a
415 character. Can't flush it. */
416 /* Open all requested <span> tags. */
417 emit_pending_spans (stream, true);
418 verify_invariants (stream);
419 }
420
421 /* Constructor. */
422
423 html_ostream_t
html_ostream_create(ostream_t destination)424 html_ostream_create (ostream_t destination)
425 {
426 html_ostream_t stream = XMALLOC (struct html_ostream_representation);
427
428 stream->base.vtable = &html_ostream_vtable;
429 stream->destination = destination;
430 stream->hyperlink_ref = NULL;
431 stream->class_stack =
432 gl_list_create_empty (GL_ARRAY_LIST, NULL, NULL, NULL, true);
433 stream->curr_class_stack_size = 0;
434 stream->last_class_stack_size = 0;
435 stream->buflen = 0;
436
437 return stream;
438 }
439