/* HTML tables renderer */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include
#include "elinks.h"
#include "document/html/parser/parse.h"
#include "document/html/parser/table.h"
#include "document/html/parser.h"
#include "document/html/renderer.h"
#include "document/html/tables.h"
#include "document/options.h"
#include "terminal/draw.h"
#include "util/color.h"
#include "util/conv.h"
#include "util/error.h"
#include "util/memory.h"
#include "util/string.h"
/* Unsafe macros */
#include "document/html/internal.h"
struct table_frames {
unsigned int top:1;
unsigned int bottom:1;
unsigned int left:1;
unsigned int right:1;
};
static void
get_table_frames(struct table *table, struct table_frames *result)
{
assert(table && result);
if (table->border) {
result->top = !!(table->frame & TABLE_FRAME_ABOVE);
result->bottom = !!(table->frame & TABLE_FRAME_BELOW);
result->left = !!(table->frame & TABLE_FRAME_LHS);
result->right = !!(table->frame & TABLE_FRAME_RHS);
} else {
memset(result, 0, sizeof(*result));
}
}
/* Distance of the table from the left margin. */
static int
get_table_indent(struct html_context *html_context, struct table *table)
{
int width = par_format.width - table->real_width;
int indent;
switch (table->align) {
case ALIGN_CENTER:
indent = (width + par_format.leftmargin - par_format.rightmargin) / 2;
break;
case ALIGN_RIGHT:
indent = width - par_format.rightmargin;
break;
case ALIGN_LEFT:
case ALIGN_JUSTIFY:
default:
indent = par_format.leftmargin;
}
/* Don't use int_bounds(&x, 0, width) here,
* width may be < 0. --Zas */
if (indent > width) indent = width;
if (indent < 0) indent = 0;
return indent;
}
static inline struct part *
format_cell(struct html_context *html_context, struct table *table,
struct table_cell *cell, struct document *document,
int x, int y, int width)
{
if (document) {
x += table->part->box.x;
y += table->part->box.y;
}
return format_html_part(html_context, cell->start, cell->end,
cell->align, table->cellpadding, width,
document, x, y, NULL, cell->link_num);
}
static inline void
get_cell_width(struct html_context *html_context,
unsigned char *start, unsigned char *end,
int cellpadding, int width,
int a, int *min, int *max,
int link_num, int *new_link_num)
{
struct part *part;
if (min) *min = -1;
if (max) *max = -1;
if (new_link_num) *new_link_num = link_num;
part = format_html_part(html_context, start, end, ALIGN_LEFT,
cellpadding, width, NULL,
!!a, !!a, NULL, link_num);
if (!part) return;
if (min) *min = part->box.width;
if (max) *max = part->max_width;
if (new_link_num) *new_link_num = part->link_num;
if (min && max) {
assertm(*min <= *max, "get_cell_width: %d > %d", *min, *max);
}
mem_free(part);
}
static void
get_cell_widths(struct html_context *html_context, struct table *table)
{
int link_num = table->part->link_num;
if (!html_context->options->table_order) {
int col, row;
for (row = 0; row < table->rows; row++)
for (col = 0; col < table->cols; col++) {
struct table_cell *cell = CELL(table, col, row);
if (!cell->start) continue;
cell->link_num = link_num;
get_cell_width(html_context, cell->start,
cell->end, table->cellpadding,
0, 0, &cell->min_width,
&cell->max_width,
link_num, &link_num);
}
} else {
int col, row;
for (col = 0; col < table->cols; col++)
for (row = 0; row < table->rows; row++) {
struct table_cell *cell = CELL(table, col, row);
if (!cell->start) continue;
cell->link_num = link_num;
get_cell_width(html_context, cell->start,
cell->end, table->cellpadding,
0, 0, &cell->min_width,
&cell->max_width,
link_num, &link_num);
}
}
table->link_num = link_num;
}
static inline void
distribute_values(int *values, int count, int wanted, int *limits)
{
int i;
int sum = 0, d, r, t;
for (i = 0; i < count; i++) sum += values[i];
if (sum >= wanted) return;
again:
t = wanted - sum;
d = t / count;
r = t % count;
wanted = 0;
if (limits) {
for (i = 0; i < count; i++) {
int delta;
values[i] += d + (i < r);
delta = values[i] - limits[i];
if (delta > 0) {
wanted += delta;
values[i] = limits[i];
}
}
} else {
for (i = 0; i < count; i++) {
values[i] += d + (i < r);
}
}
if (wanted) {
assertm(limits, "bug in distribute_values()");
limits = NULL;
sum = 0;
goto again;
}
}
/* Returns: -1 none, 0, space, 1 line, 2 double */
static inline int
get_vline_width(struct table *table, int col)
{
int width = 0;
if (!col) return -1;
if (table->rules == TABLE_RULE_COLS || table->rules == TABLE_RULE_ALL)
width = table->cellspacing;
else if (table->rules == TABLE_RULE_GROUPS)
width = (col < table->columns_count && table->columns[col].group);
if (!width && table->cellpadding) width = -1;
return width;
}
static int
get_hline_width(struct table *table, int row)
{
if (!row) return -1;
if (table->rules == TABLE_RULE_ROWS || table->rules == TABLE_RULE_ALL) {
if (table->cellspacing || table->vcellpadding)
return table->cellspacing;
return -1;
} else if (table->rules == TABLE_RULE_GROUPS) {
int col;
for (col = 0; col < table->cols; col++)
if (CELL(table, col, row)->is_group) {
if (table->cellspacing || table->vcellpadding)
return table->cellspacing;
return -1;
}
}
return table->vcellpadding ? 0 : -1;
}
#define has_vline_width(table, col) (get_vline_width(table, col) >= 0)
#define has_hline_width(table, row) (get_hline_width(table, row) >= 0)
static int
get_column_widths(struct table *table)
{
int colspan;
if (!table->cols) return -1; /* prevents calloc(0, ...) calls */
if (!table->min_cols_widths) {
table->min_cols_widths = mem_calloc(table->cols, sizeof(*table->min_cols_widths));
if (!table->min_cols_widths) return -1;
}
if (!table->max_cols_widths) {
table->max_cols_widths = mem_calloc(table->cols, sizeof(*table->max_cols_widths));
if (!table->max_cols_widths) {
mem_free_set(&table->min_cols_widths, NULL);
return -1;
}
}
if (!table->cols_widths) {
table->cols_widths = mem_calloc(table->cols, sizeof(*table->cols_widths));
if (!table->cols_widths) {
mem_free_set(&table->min_cols_widths, NULL);
mem_free_set(&table->max_cols_widths, NULL);
return -1;
}
}
colspan = 1;
do {
int col, row;
int new_colspan = INT_MAX;
for (col = 0; col < table->cols; col++) for (row = 0; row < table->rows; row++) {
struct table_cell *cell = CELL(table, col, row);
if (cell->is_spanned || !cell->is_used) continue;
assertm(cell->colspan + col <= table->cols, "colspan out of table");
if_assert_failed return -1;
if (cell->colspan == colspan) {
int k, p = 0;
for (k = 1; k < colspan; k++)
p += has_vline_width(table, col + k);
distribute_values(&table->min_cols_widths[col],
colspan,
cell->min_width - p,
&table->max_cols_widths[col]);
distribute_values(&table->max_cols_widths[col],
colspan,
cell->max_width - p,
NULL);
for (k = 0; k < colspan; k++) {
int tmp = col + k;
int_lower_bound(&table->max_cols_widths[tmp],
table->min_cols_widths[tmp]);
}
} else if (cell->colspan > colspan
&& cell->colspan < new_colspan) {
new_colspan = cell->colspan;
}
}
colspan = new_colspan;
} while (colspan != INT_MAX);
return 0;
}
static void
get_table_width(struct table *table)
{
struct table_frames table_frames;
int min = 0;
int max = 0;
int col;
for (col = 0; col < table->cols; col++) {
int vl = has_vline_width(table, col);
min += vl + table->min_cols_widths[col];
max += vl + table->max_cols_widths[col];
if (table->cols_x[col] > table->max_cols_widths[col])
max += table->cols_x[col];
}
get_table_frames(table, &table_frames);
table->min_width = min + table_frames.left + table_frames.right;
table->max_width = max + table_frames.left + table_frames.right;
assertm(min <= max, "min(%d) > max(%d)", min, max);
/* XXX: Recovery path? --pasky */
}
/* Initialize @width and @max_width arrays depending on the @stretch_method */
static inline int
apply_stretch_method(struct table *table, int widths[], int max_widths[],
int stretch_method, int max_cols_width)
{
int col, total_width = 0;
for (col = 0; col < table->cols; col++) {
switch (stretch_method) {
case 0:
if (table->cols_widths[col] >= table->cols_x[col])
break;
widths[col] = 1;
max_widths[col] = int_min(table->cols_x[col],
table->max_cols_widths[col])
- table->cols_widths[col];
if (max_widths[col] <= 0) widths[col] = 0;
break;
case 1:
if (table->cols_x[col] > WIDTH_RELATIVE)
break;
widths[col] = WIDTH_RELATIVE - table->cols_x[col];
max_widths[col] = table->max_cols_widths[col]
- table->cols_widths[col];
if (max_widths[col] <= 0) widths[col] = 0;
break;
case 2:
if (table->cols_x[col] != WIDTH_AUTO)
break;
/* Fall-through */
case 3:
if (table->cols_widths[col] >= table->max_cols_widths[col])
break;
max_widths[col] = table->max_cols_widths[col]
- table->cols_widths[col];
if (max_cols_width) {
widths[col] = 5 + table->max_cols_widths[col] * 10 / max_cols_width;
} else {
widths[col] = 1;
}
break;
case 4:
if (table->cols_x[col] < 0)
break;
widths[col] = 1;
max_widths[col] = table->cols_x[col]
- table->cols_widths[col];
if (max_widths[col] <= 0) widths[col] = 0;
break;
case 5:
if (table->cols_x[col] >= 0)
break;
if (table->cols_x[col] <= WIDTH_RELATIVE) {
widths[col] = WIDTH_RELATIVE - table->cols_x[col];
} else {
widths[col] = 1;
}
max_widths[col] = INT_MAX;
break;
case 6:
widths[col] = 1;
max_widths[col] = INT_MAX;
break;
default:
return -1;
}
total_width += widths[col];
}
return total_width;
}
/* Stretches the table columns by distributed the @spare_width among them.
* Returns how much of @spare_width was actually distributed. */
static inline int
stretch_columns(struct table *table, int widths[], int max_widths[],
int spare_width, int total_width)
{
int total_spare_width = spare_width;
while (spare_width) {
int stretch_width = 0;
int stretch_col = -1;
int col;
for (col = 0; col < table->cols; col++) {
int col_spare_width;
if (!widths[col])
continue;
col_spare_width = total_spare_width * widths[col] / total_width;
int_bounds(&col_spare_width, 1, max_widths[col]);
if (col_spare_width > stretch_width) {
stretch_width = col_spare_width;
stretch_col = col;
}
}
/* Got stretch column? */
if (stretch_col == -1)
break;
/* Mark the column as visited */
widths[stretch_col] = 0;
if (stretch_width > spare_width)
stretch_width = spare_width;
assertm(stretch_width >= 0, "shrinking cell");
table->cols_widths[stretch_col] += stretch_width;
spare_width -= stretch_width;
}
return total_spare_width - spare_width;
}
/* This function distributes space evenly between @table columns so that it
* stretches to @width. */
static void
distribute_widths(struct table *table, int width)
{
int col;
int spare_width = width - table->min_width;
int stretch_method = 0;
int *widths, *max_widths;
int max_cols_width = 0;
int cols_array_size;
if (!table->cols)
return;
assertm(spare_width >= 0, "too small width %d, required %d",
width, table->min_width);
for (col = 0; col < table->cols; col++)
int_lower_bound(&max_cols_width, table->max_cols_widths[col]);
cols_array_size = table->cols * sizeof(*table->cols_widths);
memcpy(table->cols_widths, table->min_cols_widths, cols_array_size);
table->real_width = width;
widths = fmem_alloc(cols_array_size);
if (!widths) return;
max_widths = fmem_alloc(cols_array_size);
if (!max_widths) goto free_widths;
while (spare_width) {
int stretched, total_width;
memset(widths, 0, cols_array_size);
memset(max_widths, 0, cols_array_size);
total_width = apply_stretch_method(table, widths, max_widths, stretch_method, max_cols_width);
assertm(total_width != -1, "Could not expand table");
if_assert_failed break;
if (!total_width) {
stretch_method++;
continue;
}
stretched = stretch_columns(table, widths, max_widths, spare_width, total_width);
if (!stretched)
stretch_method++;
else
spare_width -= stretched;
}
fmem_free(max_widths);
free_widths:
fmem_free(widths);
}
static int
get_table_cellpadding(struct html_context *html_context, struct table *table)
{
struct part *part = table->part;
int cpd_pass = 0, cpd_width = 0, cpd_last = table->cellpadding;
int margins = par_format.leftmargin + par_format.rightmargin;
again:
get_cell_widths(html_context, table);
if (get_column_widths(table)) return -1;
get_table_width(table);
if (!part->document && !part->box.x) {
if (!table->full_width)
int_upper_bound(&table->max_width, table->width);
int_lower_bound(&table->max_width, table->min_width);
int_lower_bound(&part->max_width, table->max_width + margins);
int_lower_bound(&part->box.width, table->min_width + margins);
return -1;
}
if (!cpd_pass && table->min_width > table->width && table->cellpadding) {
table->cellpadding = 0;
cpd_pass = 1;
cpd_width = table->min_width;
goto again;
}
if (cpd_pass == 1 && table->min_width > cpd_width) {
table->cellpadding = cpd_last;
cpd_pass = 2;
goto again;
}
return 0;
}
#ifdef HTML_TABLE_2ND_PASS /* This is by default ON! () */
static void
check_table_widths(struct html_context *html_context, struct table *table)
{
int col, row;
int colspan;
int width, new_width;
int max, max_index = 0; /* go away, warning! */
int *widths = mem_calloc(table->cols, sizeof(*widths));
if (!widths) return;
for (row = 0; row < table->rows; row++) for (col = 0; col < table->cols; col++) {
struct table_cell *cell = CELL(table, col, row);
int k, p = 0;
if (!cell->start) continue;
for (k = 0; k < cell->colspan; k++) {
p += table->cols_widths[col + k] +
(k && has_vline_width(table, col + k));
}
get_cell_width(html_context, cell->start, cell->end,
table->cellpadding, p, 1, &cell->width,
NULL, cell->link_num, NULL);
int_upper_bound(&cell->width, p);
}
colspan = 1;
do {
int new_colspan = INT_MAX;
for (col = 0; col < table->cols; col++) for (row = 0; row < table->rows; row++) {
struct table_cell *cell = CELL(table, col, row);
if (!cell->start) continue;
assertm(cell->colspan + col <= table->cols, "colspan out of table");
if_assert_failed goto end;
if (cell->colspan == colspan) {
int k, p = 0;
for (k = 1; k < colspan; k++)
p += has_vline_width(table, col + k);
distribute_values(&widths[col],
colspan,
cell->width - p,
&table->max_cols_widths[col]);
} else if (cell->colspan > colspan
&& cell->colspan < new_colspan) {
new_colspan = cell->colspan;
}
}
colspan = new_colspan;
} while (colspan != INT_MAX);
width = new_width = 0;
for (col = 0; col < table->cols; col++) {
width += table->cols_widths[col];
new_width += widths[col];
}
if (new_width > width) {
/* INTERNAL("new width(%d) is larger than previous(%d)", new_width, width); */
goto end;
}
max = -1;
for (col = 0; col < table->cols; col++)
if (table->max_cols_widths[col] > max) {
max = table->max_cols_widths[col];
max_index = col;
}
if (max != -1) {
widths[max_index] += width - new_width;
if (widths[max_index] <= table->max_cols_widths[max_index]) {
mem_free(table->cols_widths);
table->cols_widths = widths;
return;
}
}
end:
mem_free(widths);
}
#endif
static void
check_table_height(struct table *table, struct table_frames *frames, int y)
{
#ifndef CONFIG_FASTMEM
/* XXX: Cannot we simply use the @yp value we just calculated
* in draw_table_cells()? --pasky */
int old_height = table->real_height + table->part->cy;
int our_height = frames->top + y + frames->bottom + table->caption_height;
int row;
/* XXX: We cannot use get_table_real_height() because we are
* looking one row ahead - which is completely arcane to me.
* It makes a difference only when a table uses ruler="groups"
* and has non-zero cellspacing or vcellpadding. --pasky */
for (row = 0; row < table->rows; row++) {
our_height += table->rows_heights[row] +
(row < table->rows - 1 &&
has_hline_width(table, row + 1));
}
assertm(old_height == our_height, "size not matching! %d vs %d",
old_height, our_height);
#endif
}
static int
get_table_caption_height(struct html_context *html_context, struct table *table)
{
unsigned char *start = table->caption.start;
unsigned char *end = table->caption.end;
struct part *part;
if (!start || !end) return 0;
while (start < end && isspace(*start))
start++;
while (start < end && isspace(end[-1]))
end--;
if (start >= end) return 0;
part = format_html_part(html_context, start, end, table->align,
0, table->real_width, NULL, 0, 0,
NULL, table->link_num);
if (!part) {
return 0;
} else {
int height = part->box.height;
mem_free(part);
return height;
}
}
static int
get_table_real_height(struct table *table)
{
struct table_frames table_frames;
int height;
int row;
get_table_frames(table, &table_frames);
height = table_frames.top + table_frames.bottom;
height += table->caption_height;
for (row = 0; row < table->rows; row++) {
height += table->rows_heights[row];
if (row && has_hline_width(table, row))
height++;
}
return height;
}
static void
get_table_heights(struct html_context *html_context, struct table *table)
{
int rowspan;
int col, row;
table->caption_height = get_table_caption_height(html_context, table);
for (row = 0; row < table->rows; row++) {
for (col = 0; col < table->cols; col++) {
struct table_cell *cell = CELL(table, col, row);
struct part *part;
int width = 0, sp;
if (!cell->is_used || cell->is_spanned) continue;
for (sp = 0; sp < cell->colspan; sp++) {
width += table->cols_widths[col + sp] +
(sp < cell->colspan - 1 &&
has_vline_width(table, col + sp + 1));
}
part = format_cell(html_context, table, cell, NULL,
2, 2, width);
if (!part) return;
cell->height = part->box.height;
/* DBG("%d, %d.", width, cell->height); */
mem_free(part);
}
}
rowspan = 1;
do {
int new_rowspan = INT_MAX;
for (row = 0; row < table->rows; row++) {
for (col = 0; col < table->cols; col++) {
struct table_cell *cell = CELL(table, col, row);
if (!cell->is_used || cell->is_spanned) continue;
if (cell->rowspan == rowspan) {
int k, p = 0;
for (k = 1; k < rowspan; k++)
p += has_hline_width(table, row + k);
distribute_values(&table->rows_heights[row],
rowspan,
cell->height - p,
NULL);
} else if (cell->rowspan > rowspan &&
cell->rowspan < new_rowspan) {
new_rowspan = cell->rowspan;
}
}
}
rowspan = new_rowspan;
} while (rowspan != INT_MAX);
table->real_height = get_table_real_height(table);
}
static void
draw_table_cell(struct table *table, int col, int row, int x, int y,
struct html_context *html_context)
{
struct table_cell *cell = CELL(table, col, row);
struct document *document = table->part->document;
struct part *part;
int width = 0;
int height = 0;
int s, tmpy = y;
struct html_element *state;
if (!cell->start) return;
for (s = 0; s < cell->colspan; s++) {
width += table->cols_widths[col + s] +
(s < cell->colspan - 1 &&
has_vline_width(table, col + s + 1));
}
for (s = 0; s < cell->rowspan; s++) {
height += table->rows_heights[row + s] +
(s < cell->rowspan - 1 &&
has_hline_width(table, row + s + 1));
}
state = init_html_parser_state(html_context, ELEMENT_DONT_KILL,
cell->align, 0, 0);
if (cell->is_header) format.style.attr |= AT_BOLD;
format.style.bg = cell->bgcolor;
par_format.bgcolor = cell->bgcolor;
if (cell->valign == VALIGN_MIDDLE)
tmpy += (height - cell->height) / 2;
else if (cell->valign == VALIGN_BOTTOM)
tmpy += (height - cell->height);
part = format_cell(html_context, table, cell, document, x, tmpy, width);
if (part) {
/* The cell content doesn't necessarily fill out the whole cell
* height so use the calculated @height because it is an upper
* bound. */
assert(height >= cell->height);
/* The line expansion draws the _remaining_ background color of
* both untouched lines and lines that doesn't stretch the
* whole cell width. */
expand_lines(html_context, table->part, x + width - 1, y, height, cell->bgcolor);
if (cell->fragment_id)
add_fragment_identifier(html_context, part,
cell->fragment_id);
}
done_html_parser_state(html_context, state);
if (part) mem_free(part);
}
static void
draw_table_cells(struct table *table, int x, int y,
struct html_context *html_context)
{
int col, row;
int xp;
color_T bgcolor = par_format.bgcolor;
struct table_frames table_frames;
get_table_frames(table, &table_frames);
if (table->fragment_id)
add_fragment_identifier(html_context, table->part,
table->fragment_id);
/* Expand using the background color of the ``parent context'' all the
* way down the start of the left edge of the table. */
expand_lines(html_context, table->part, x - 1, y, table->real_height, bgcolor);
xp = x + table_frames.left;
for (col = 0; col < table->cols; col++) {
int yp = y + table_frames.top;
for (row = 0; row < table->rows; row++) {
int row_height = table->rows_heights[row] +
(row < table->rows - 1 && has_hline_width(table, row + 1));
draw_table_cell(table, col, row, xp, yp, html_context);
yp += row_height;
}
if (col < table->cols - 1) {
xp += table->cols_widths[col] + has_vline_width(table, col + 1);
}
}
/* Finish the table drawing by aligning the right and bottom edge of
* the table */
x += table->real_width - 1;
expand_lines(html_context, table->part, x, y, table->real_height, table->bgcolor);
/* Tables are renderer column-wise which breaks forms where the
* form items appears in a column before the actual form tag is
* parsed. Consider the folloing example:
*
* +--------+--------+ Where cell 2 has a