1 /* HTML forms parser */
2
3 #ifdef HAVE_CONFIG_H
4 #include "config.h"
5 #endif
6
7 #include <stdarg.h>
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11
12 #include "elinks.h"
13
14 #include "bfu/listmenu.h"
15 #include "bfu/menu.h"
16 #include "document/html/parser/forms.h"
17 #include "document/html/parser/link.h"
18 #include "document/html/parser/stack.h"
19 #include "document/html/parser/parse.h"
20 #include "document/html/parser.h"
21 #include "document/html/renderer.h"
22 #include "document/forms.h"
23 #include "intl/charsets.h"
24 #include "protocol/protocol.h"
25 #include "protocol/uri.h"
26 #include "util/conv.h"
27 #include "util/error.h"
28 #include "util/memdebug.h"
29 #include "util/memlist.h"
30 #include "util/memory.h"
31 #include "util/string.h"
32
33 /* Unsafe macros */
34 #include "document/html/internal.h"
35
36
37
38 void
html_form(struct html_context * html_context,unsigned char * a,unsigned char * xxx3,unsigned char * xxx4,unsigned char ** xxx5)39 html_form(struct html_context *html_context, unsigned char *a,
40 unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
41 {
42 unsigned char *al;
43 struct form *form;
44
45 html_context->was_br = 1;
46
47 form = init_form();
48 if (!form) return;
49
50 form->method = FORM_METHOD_GET;
51 form->form_num = a - html_context->startf;
52
53 al = get_attr_val(a, "method", html_context->options);
54 if (al) {
55 if (!c_strcasecmp(al, "post")) {
56 unsigned char *enctype;
57
58 enctype = get_attr_val(a, "enctype",
59 html_context->options);
60
61 form->method = FORM_METHOD_POST;
62 if (enctype) {
63 if (!c_strcasecmp(enctype, "multipart/form-data"))
64 form->method = FORM_METHOD_POST_MP;
65 else if (!c_strcasecmp(enctype, "text/plain"))
66 form->method = FORM_METHOD_POST_TEXT_PLAIN;
67 mem_free(enctype);
68 }
69 }
70 mem_free(al);
71 }
72
73 al = get_attr_val(a, "name", html_context->options);
74 if (al) form->name = al;
75
76 al = get_attr_val(a, "action", html_context->options);
77 /* The HTML specification at
78 * http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.3 states
79 * that the behavior of an empty action attribute should be undefined.
80 * Mozilla handles action="" as action="<current-URI>" which seems
81 * reasonable. (bug 615) */
82 if (al && *al) {
83 form->action = join_urls(html_context->base_href, trim_chars(al, ' ', NULL));
84 mem_free(al);
85
86 } else {
87 enum uri_component components = URI_ORIGINAL;
88
89 mem_free_if(al);
90
91 /* We have to do following for GET method, because we would end
92 * up with two '?' otherwise. */
93 if (form->method == FORM_METHOD_GET)
94 components = URI_FORM_GET;
95
96 form->action = get_uri_string(html_context->base_href, components);
97
98 /* No action URI should contain post data */
99 assert(!form->action || !strchr(form->action, POST_CHAR));
100
101 /* GET method URIs should not have '?'. */
102 assert(!form->action
103 || form->method != FORM_METHOD_GET
104 || !strchr(form->action, '?'));
105 }
106
107 al = get_target(html_context->options, a);
108 form->target = al ? al : stracpy(html_context->base_target);
109
110 html_context->special_f(html_context, SP_FORM, form);
111 }
112
113
114 static int
get_form_mode(struct html_context * html_context,unsigned char * attr)115 get_form_mode(struct html_context *html_context, unsigned char *attr)
116 {
117 if (has_attr(attr, "disabled", html_context->options))
118 return FORM_MODE_DISABLED;
119
120 if (has_attr(attr, "readonly", html_context->options))
121 return FORM_MODE_READONLY;
122
123 return FORM_MODE_NORMAL;
124 }
125
126 static struct form_control *
init_form_control(enum form_type type,unsigned char * attr,struct html_context * html_context)127 init_form_control(enum form_type type, unsigned char *attr,
128 struct html_context *html_context)
129 {
130 struct form_control *fc;
131
132 fc = mem_calloc(1, sizeof(*fc));
133 if (!fc) return NULL;
134
135 fc->type = type;
136 fc->position = attr - html_context->startf;
137 fc->mode = get_form_mode(html_context, attr);
138
139 return fc;
140 }
141
142 void
html_button(struct html_context * html_context,unsigned char * a,unsigned char * xxx3,unsigned char * xxx4,unsigned char ** xxx5)143 html_button(struct html_context *html_context, unsigned char *a,
144 unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
145 {
146 unsigned char *al;
147 struct form_control *fc;
148 enum form_type type = FC_SUBMIT;
149
150 html_focusable(html_context, a);
151
152 al = get_attr_val(a, "type", html_context->options);
153 if (!al) goto no_type_attr;
154
155 if (!c_strcasecmp(al, "button")) {
156 type = FC_BUTTON;
157 } else if (!c_strcasecmp(al, "reset")) {
158 type = FC_RESET;
159 } else if (c_strcasecmp(al, "submit")) {
160 /* unknown type */
161 mem_free(al);
162 return;
163 }
164 mem_free(al);
165
166 no_type_attr:
167 fc = init_form_control(type, a, html_context);
168 if (!fc) return;
169
170 fc->name = get_attr_val(a, "name", html_context->options);
171 fc->default_value = get_attr_val(a, "value", html_context->options);
172 if (!fc->default_value) {
173 if (fc->type == FC_SUBMIT)
174 fc->default_value = stracpy("Submit");
175 else if (fc->type == FC_RESET)
176 fc->default_value = stracpy("Reset");
177 else if (fc->type == FC_BUTTON)
178 fc->default_value = stracpy("Button");
179 }
180 if (!fc->default_value)
181 fc->default_value = stracpy("");
182
183 html_context->special_f(html_context, SP_CONTROL, fc);
184 format.form = fc;
185 format.style.attr |= AT_BOLD;
186 }
187
188 static void
html_input_format(struct html_context * html_context,unsigned char * a,struct form_control * fc)189 html_input_format(struct html_context *html_context, unsigned char *a,
190 struct form_control *fc)
191 {
192 put_chrs(html_context, " ", 1);
193 html_stack_dup(html_context, ELEMENT_KILLABLE);
194 html_focusable(html_context, a);
195 format.form = fc;
196 if (format.title) mem_free(format.title);
197 format.title = get_attr_val(a, "title", html_context->options);
198 switch (fc->type) {
199 case FC_TEXT:
200 case FC_PASSWORD:
201 case FC_FILE:
202 {
203 int i;
204
205 format.style.attr |= AT_BOLD;
206 for (i = 0; i < fc->size; i++)
207 put_chrs(html_context, "_", 1);
208 break;
209 }
210 case FC_CHECKBOX:
211 format.style.attr |= AT_BOLD;
212 put_chrs(html_context, "[ ]", 8);
213 break;
214 case FC_RADIO:
215 format.style.attr |= AT_BOLD;
216 put_chrs(html_context, "( )", 8);
217 break;
218 case FC_IMAGE:
219 {
220 unsigned char *al;
221
222 mem_free_set(&format.image, NULL);
223 al = get_url_val(a, "src", html_context->options);
224 if (!al)
225 al = get_url_val(a, "dynsrc",
226 html_context->options);
227 if (al) {
228 format.image = join_urls(html_context->base_href, al);
229 mem_free(al);
230 }
231 format.style.attr |= AT_BOLD;
232 put_chrs(html_context, "[ ", 7);
233 if (fc->alt)
234 put_chrs(html_context, fc->alt, strlen(fc->alt));
235 else if (fc->name)
236 put_chrs(html_context, fc->name, strlen(fc->name));
237 else
238 put_chrs(html_context, "Submit", 6);
239
240 put_chrs(html_context, " ]", 7);
241 break;
242 }
243 case FC_SUBMIT:
244 case FC_RESET:
245 case FC_BUTTON:
246 format.style.attr |= AT_BOLD;
247 put_chrs(html_context, "[ ", 7);
248 if (fc->default_value)
249 put_chrs(html_context, fc->default_value, strlen(fc->default_value));
250 put_chrs(html_context, " ]", 7);
251 break;
252 case FC_TEXTAREA:
253 case FC_SELECT:
254 case FC_HIDDEN:
255 INTERNAL("bad control type");
256 }
257 kill_html_stack_item(html_context, &html_top);
258 put_chrs(html_context, " ", 1);
259 }
260
261 void
html_input(struct html_context * html_context,unsigned char * a,unsigned char * xxx3,unsigned char * xxx4,unsigned char ** xxx5)262 html_input(struct html_context *html_context, unsigned char *a,
263 unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
264 {
265 unsigned char *al;
266 struct form_control *fc;
267
268 fc = init_form_control(FC_TEXT, a, html_context);
269 if (!fc) return;
270
271 al = get_attr_val(a, "type", html_context->options);
272 if (al) {
273 if (!c_strcasecmp(al, "text")) fc->type = FC_TEXT;
274 else if (!c_strcasecmp(al, "hidden")) fc->type = FC_HIDDEN;
275 else if (!c_strcasecmp(al, "button")) fc->type = FC_BUTTON;
276 else if (!c_strcasecmp(al, "checkbox")) fc->type = FC_CHECKBOX;
277 else if (!c_strcasecmp(al, "radio")) fc->type = FC_RADIO;
278 else if (!c_strcasecmp(al, "password")) fc->type = FC_PASSWORD;
279 else if (!c_strcasecmp(al, "submit")) fc->type = FC_SUBMIT;
280 else if (!c_strcasecmp(al, "reset")) fc->type = FC_RESET;
281 else if (!c_strcasecmp(al, "file")) fc->type = FC_FILE;
282 else if (!c_strcasecmp(al, "image")) fc->type = FC_IMAGE;
283 /* else unknown type, let it default to FC_TEXT. */
284 mem_free(al);
285 }
286
287 if (fc->type != FC_FILE)
288 fc->default_value = get_attr_val(a, "value",
289 html_context->options);
290 if (!fc->default_value) {
291 if (fc->type == FC_CHECKBOX)
292 fc->default_value = stracpy("on");
293 else if (fc->type == FC_SUBMIT)
294 fc->default_value = stracpy("Submit");
295 else if (fc->type == FC_RESET)
296 fc->default_value = stracpy("Reset");
297 else if (fc->type == FC_BUTTON)
298 fc->default_value = stracpy("Button");
299 }
300 if (!fc->default_value)
301 fc->default_value = stracpy("");
302
303 fc->name = get_attr_val(a, "name", html_context->options);
304
305 fc->size = get_num(a, "size", html_context->options);
306 if (fc->size == -1)
307 fc->size = html_context->options->default_form_input_size;
308 fc->size++;
309 if (fc->size > html_context->options->box.width)
310 fc->size = html_context->options->box.width;
311 fc->maxlength = get_num(a, "maxlength", html_context->options);
312 if (fc->maxlength == -1) fc->maxlength = INT_MAX;
313 if (fc->type == FC_CHECKBOX || fc->type == FC_RADIO)
314 fc->default_state = has_attr(a, "checked",
315 html_context->options);
316 if (fc->type == FC_IMAGE)
317 fc->alt = get_attr_val(a, "alt", html_context->options);
318
319 if (fc->type != FC_HIDDEN) {
320 html_input_format(html_context, a, fc);
321 }
322
323 html_context->special_f(html_context, SP_CONTROL, fc);
324 }
325
326 static struct list_menu lnk_menu;
327
328 static void
do_html_select(unsigned char * attr,unsigned char * html,unsigned char * eof,unsigned char ** end,struct html_context * html_context)329 do_html_select(unsigned char *attr, unsigned char *html,
330 unsigned char *eof, unsigned char **end,
331 struct html_context *html_context)
332 {
333 struct conv_table *ct = html_context->special_f(html_context, SP_TABLE, NULL);
334 struct form_control *fc;
335 struct string lbl = NULL_STRING, orig_lbl = NULL_STRING;
336 unsigned char **values = NULL;
337 unsigned char **labels;
338 unsigned char *name, *t_attr, *en;
339 int namelen;
340 int nnmi = 0;
341 int order = 0;
342 int preselect = -1;
343 int group = 0;
344 int i, max_width;
345 int closing_tag;
346
347 html_focusable(html_context, attr);
348 init_menu(&lnk_menu);
349
350 se:
351 en = html;
352
353 see:
354 html = en;
355 while (html < eof && *html != '<') html++;
356
357 if (html >= eof) {
358
359 abort:
360 *end = html;
361 if (lbl.source) done_string(&lbl);
362 if (orig_lbl.source) done_string(&orig_lbl);
363 if (values) {
364 int j;
365
366 for (j = 0; j < order; j++)
367 mem_free_if(values[j]);
368 mem_free(values);
369 }
370 destroy_menu(&lnk_menu);
371 *end = en;
372 return;
373 }
374
375 if (lbl.source) {
376 unsigned char *q, *s = en;
377 int l = html - en;
378
379 while (l && isspace(s[0])) s++, l--;
380 while (l && isspace(s[l-1])) l--;
381 q = convert_string(ct, s, l,
382 html_context->options->cp,
383 CSM_DEFAULT, NULL, NULL, NULL);
384 if (q) add_to_string(&lbl, q), mem_free(q);
385 add_bytes_to_string(&orig_lbl, s, l);
386 }
387
388 if (html + 2 <= eof && (html[1] == '!' || html[1] == '?')) {
389 html = skip_comment(html, eof);
390 goto se;
391 }
392
393 if (parse_element(html, eof, &name, &namelen, &t_attr, &en)) {
394 html++;
395 goto se;
396 }
397
398 if (!namelen) goto see;
399
400 if (name[0] == '/') {
401 namelen--;
402 if (!namelen) goto see;
403 name++;
404 closing_tag = 1;
405 } else {
406 closing_tag = 0;
407 }
408
409 if (closing_tag && !c_strlcasecmp(name, namelen, "SELECT", 6)) {
410 add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);
411 goto end_parse;
412 }
413
414 if (!c_strlcasecmp(name, namelen, "OPTION", 6)) {
415 add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);
416
417 if (!closing_tag) {
418 unsigned char *value, *label;
419
420 if (has_attr(t_attr, "disabled", html_context->options))
421 goto see;
422 if (preselect == -1
423 && has_attr(t_attr, "selected", html_context->options))
424 preselect = order;
425 value = get_attr_val(t_attr, "value", html_context->options);
426
427 if (!mem_align_alloc(&values, order, order + 1, unsigned char *, 0xFF))
428 goto abort;
429
430 values[order++] = value;
431 label = get_attr_val(t_attr, "label", html_context->options);
432 if (label) new_menu_item(&lnk_menu, label, order - 1, 0);
433 if (!value || !label) {
434 init_string(&lbl);
435 init_string(&orig_lbl);
436 nnmi = !!label;
437 }
438 }
439
440 goto see;
441 }
442
443 if (!c_strlcasecmp(name, namelen, "OPTGROUP", 8)) {
444 add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);
445
446 if (group) new_menu_item(&lnk_menu, NULL, -1, 0), group = 0;
447
448 if (!closing_tag) {
449 unsigned char *label;
450
451 label = get_attr_val(t_attr, "label", html_context->options);
452
453 if (!label) {
454 label = stracpy("");
455 if (!label) goto see;
456 }
457 new_menu_item(&lnk_menu, label, -1, 0);
458 group = 1;
459 }
460 }
461
462 goto see;
463
464
465 end_parse:
466 *end = en;
467 if (!order) goto abort;
468
469 labels = mem_calloc(order, sizeof(unsigned char *));
470 if (!labels) goto abort;
471
472 fc = init_form_control(FC_SELECT, attr, html_context);
473 if (!fc) {
474 mem_free(labels);
475 goto abort;
476 }
477
478 fc->name = get_attr_val(attr, "name", html_context->options);
479 fc->default_state = preselect < 0 ? 0 : preselect;
480 fc->default_value = order ? stracpy(values[fc->default_state]) : stracpy("");
481 fc->nvalues = order;
482 fc->values = values;
483 fc->menu = detach_menu(&lnk_menu);
484 fc->labels = labels;
485
486 menu_labels(fc->menu, "", labels);
487 put_chrs(html_context, "[", 1);
488 html_stack_dup(html_context, ELEMENT_KILLABLE);
489 format.form = fc;
490 format.style.attr |= AT_BOLD;
491
492 max_width = 0;
493 for (i = 0; i < order; i++) {
494 if (!labels[i]) continue;
495 int_lower_bound(&max_width, strlen(labels[i]));
496 }
497
498 for (i = 0; i < max_width; i++)
499 put_chrs(html_context, "_", 1);
500
501 kill_html_stack_item(html_context, &html_top);
502 put_chrs(html_context, "]", 1);
503 html_context->special_f(html_context, SP_CONTROL, fc);
504 }
505
506
507 static void
do_html_select_multiple(struct html_context * html_context,unsigned char * a,unsigned char * html,unsigned char * eof,unsigned char ** end)508 do_html_select_multiple(struct html_context *html_context, unsigned char *a,
509 unsigned char *html, unsigned char *eof,
510 unsigned char **end)
511 {
512 unsigned char *al = get_attr_val(a, "name", html_context->options);
513
514 if (!al) return;
515 html_focusable(html_context, a);
516 html_top.type = ELEMENT_DONT_KILL;
517 mem_free_set(&format.select, al);
518 format.select_disabled = has_attr(a, "disabled", html_context->options)
519 ? FORM_MODE_DISABLED
520 : FORM_MODE_NORMAL;
521 }
522
523 void
html_select(struct html_context * html_context,unsigned char * a,unsigned char * html,unsigned char * eof,unsigned char ** end)524 html_select(struct html_context *html_context, unsigned char *a,
525 unsigned char *html, unsigned char *eof, unsigned char **end)
526 {
527 if (has_attr(a, "multiple", html_context->options))
528 do_html_select_multiple(html_context, a, html, eof, end);
529 else
530 do_html_select(a, html, eof, end, html_context);
531
532 }
533
534 void
html_option(struct html_context * html_context,unsigned char * a,unsigned char * xxx3,unsigned char * xxx4,unsigned char ** xxx5)535 html_option(struct html_context *html_context, unsigned char *a,
536 unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
537 {
538 struct form_control *fc;
539 unsigned char *val;
540
541 if (!format.select) return;
542
543 val = get_attr_val(a, "value", html_context->options);
544 if (!val) {
545 struct string str;
546 unsigned char *p, *r;
547 unsigned char *name;
548 int namelen;
549
550 for (p = a - 1; *p != '<'; p--);
551
552 if (!init_string(&str)) goto end_parse;
553 if (parse_element(p, html_context->eoff, NULL, NULL, NULL, &p)) {
554 INTERNAL("parse element failed");
555 val = str.source;
556 goto end_parse;
557 }
558
559 se:
560 while (p < html_context->eoff && isspace(*p)) p++;
561 while (p < html_context->eoff && !isspace(*p) && *p != '<') {
562
563 sp:
564 add_char_to_string(&str, *p ? *p : ' '), p++;
565 }
566
567 r = p;
568 val = str.source; /* Has to be before the possible 'goto end_parse' */
569
570 while (r < html_context->eoff && isspace(*r)) r++;
571 if (r >= html_context->eoff) goto end_parse;
572 if (r - 2 <= html_context->eoff && (r[1] == '!' || r[1] == '?')) {
573 p = skip_comment(r, html_context->eoff);
574 goto se;
575 }
576 if (parse_element(r, html_context->eoff, &name, &namelen, NULL, &p)) goto sp;
577
578 if (namelen < 6) goto se;
579 if (name[0] == '/') name++, namelen--;
580
581 if (c_strlcasecmp(name, namelen, "OPTION", 6)
582 && c_strlcasecmp(name, namelen, "SELECT", 6)
583 && c_strlcasecmp(name, namelen, "OPTGROUP", 8))
584 goto se;
585 }
586
587 end_parse:
588 fc = init_form_control(FC_CHECKBOX, a, html_context);
589 if (!fc) {
590 mem_free_if(val);
591 return;
592 }
593
594 fc->name = null_or_stracpy(format.select);
595 fc->default_value = val;
596 fc->default_state = has_attr(a, "selected", html_context->options);
597 fc->mode = has_attr(a, "disabled", html_context->options)
598 ? FORM_MODE_DISABLED
599 : format.select_disabled;
600
601 put_chrs(html_context, " ", 1);
602 html_stack_dup(html_context, ELEMENT_KILLABLE);
603 format.form = fc;
604 format.style.attr |= AT_BOLD;
605 put_chrs(html_context, "[ ]", 3);
606 kill_html_stack_item(html_context, &html_top);
607 put_chrs(html_context, " ", 1);
608 html_context->special_f(html_context, SP_CONTROL, fc);
609 }
610
611 void
html_textarea(struct html_context * html_context,unsigned char * attr,unsigned char * html,unsigned char * eof,unsigned char ** end)612 html_textarea(struct html_context *html_context, unsigned char *attr,
613 unsigned char *html, unsigned char *eof, unsigned char **end)
614 {
615 struct form_control *fc;
616 unsigned char *p, *t_name, *wrap_attr;
617 int t_namelen;
618 int cols, rows;
619 int i;
620
621 html_focusable(html_context, attr);
622 while (html < eof && (*html == '\n' || *html == '\r')) html++;
623 p = html;
624 while (p < eof && *p != '<') {
625
626 pp:
627 p++;
628 }
629 if (p >= eof) {
630 *end = eof;
631 return;
632 }
633 if (parse_element(p, eof, &t_name, &t_namelen, NULL, end)) goto pp;
634 if (c_strlcasecmp(t_name, t_namelen, "/TEXTAREA", 9)) goto pp;
635
636 fc = init_form_control(FC_TEXTAREA, attr, html_context);
637 if (!fc) return;
638
639 fc->name = get_attr_val(attr, "name", html_context->options);
640 fc->default_value = memacpy(html, p - html);
641 for (p = fc->default_value; p && p[0]; p++) {
642 /* FIXME: We don't cope well with entities here. Bugzilla uses
643 * inside of textarea and we fail miserably upon that
644 * one. --pasky */
645 if (p[0] == '\r') {
646 if (p[1] == '\n'
647 || (p > fc->default_value && p[-1] == '\n')) {
648 memmove(p, p + 1, strlen(p));
649 p--;
650 } else {
651 p[0] = '\n';
652 }
653 }
654 }
655
656 cols = get_num(attr, "cols", html_context->options);
657 if (cols <= 0)
658 cols = html_context->options->default_form_input_size;
659 cols++; /* Add 1 column, other browsers may have different
660 behavior here (mozilla adds 2) --Zas */
661 if (cols > html_context->options->box.width)
662 cols = html_context->options->box.width;
663 fc->cols = cols;
664
665 rows = get_num(attr, "rows", html_context->options);
666 if (rows <= 0) rows = 1;
667 if (rows > html_context->options->box.height)
668 rows = html_context->options->box.height;
669 fc->rows = rows;
670 html_context->options->needs_height = 1;
671
672 wrap_attr = get_attr_val(attr, "wrap", html_context->options);
673 if (wrap_attr) {
674 if (!c_strcasecmp(wrap_attr, "hard")
675 || !c_strcasecmp(wrap_attr, "physical")) {
676 fc->wrap = FORM_WRAP_HARD;
677 } else if (!c_strcasecmp(wrap_attr, "soft")
678 || !c_strcasecmp(wrap_attr, "virtual")) {
679 fc->wrap = FORM_WRAP_SOFT;
680 } else if (!c_strcasecmp(wrap_attr, "none")
681 || !c_strcasecmp(wrap_attr, "off")) {
682 fc->wrap = FORM_WRAP_NONE;
683 }
684 mem_free(wrap_attr);
685
686 } else if (has_attr(attr, "nowrap", html_context->options)) {
687 fc->wrap = FORM_WRAP_NONE;
688
689 } else {
690 fc->wrap = FORM_WRAP_SOFT;
691 }
692
693 fc->maxlength = get_num(attr, "maxlength", html_context->options);
694 if (fc->maxlength == -1) fc->maxlength = INT_MAX;
695
696 if (rows > 1) ln_break(html_context, 1);
697 else put_chrs(html_context, " ", 1);
698
699 html_stack_dup(html_context, ELEMENT_KILLABLE);
700 format.form = fc;
701 format.style.attr |= AT_BOLD;
702
703 for (i = 0; i < rows; i++) {
704 int j;
705
706 for (j = 0; j < cols; j++)
707 put_chrs(html_context, "_", 1);
708 if (i < rows - 1)
709 ln_break(html_context, 1);
710 }
711
712 kill_html_stack_item(html_context, &html_top);
713 if (rows > 1)
714 ln_break(html_context, 1);
715 else
716 put_chrs(html_context, " ", 1);
717 html_context->special_f(html_context, SP_CONTROL, fc);
718 }
719