Skip to content

Commit

Permalink
fix!: ensure HTML ID attributes are unique (#607)
Browse files Browse the repository at this point in the history
* fix: ensure HTML ID attributes are unique

* Use column names for IDs instead of labels

---------

Co-authored-by: Richard Iannone <[email protected]>
  • Loading branch information
BenGale93 and rich-iannone authored Feb 21, 2025
1 parent ac1ea7e commit dc82197
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 25 deletions.
2 changes: 1 addition & 1 deletion great_tables/_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _process_text(x: str | BaseText | None, context: str = "html") -> str:


def _process_text_id(x: str | BaseText | None) -> str:
return _process_text(x)
return _process_text(x).replace(" ", "-")


def _html_escape(x: str) -> str:
Expand Down
29 changes: 22 additions & 7 deletions great_tables/_utils_render_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ._gt_data import GroupRowInfo, GTData, Styles
from ._spanners import spanners_print_matrix
from ._tbl_data import _get_cell, cast_frame_to_string, replace_null_frame
from ._text import _process_text, _process_text_id
from ._text import BaseText, _process_text, _process_text_id
from ._utils import heading_has_subtitle, heading_has_title, seq_groups


Expand Down Expand Up @@ -41,6 +41,14 @@ def _flatten_styles(styles: Styles, wrap: bool = False) -> str | None:
return None


def _create_element_id(table_id: str | None, element_id: str | BaseText | None) -> str:
# Given a table ID, element IDs are prepended by it to ensure the resulting HTML
# has unique IDs.
new_table_id = table_id or ""
processed_id = _process_text_id(element_id)
return f"{new_table_id}-{processed_id}" if new_table_id and processed_id else processed_id


def create_heading_component_h(data: GTData) -> str:
title = data._heading.title
subtitle = data._heading.subtitle
Expand Down Expand Up @@ -150,6 +158,9 @@ def create_columns_component_h(data: GTData) -> str:
# Initialize the column headings list
table_col_headings = []

# Extract the table ID to ensure subsequent IDs are unique
table_id = data._options.table_id.value

# If there are no spanners, then we have to create the cells for the stubhead label
# (if present) and for the column headings
if spanner_row_count == 0:
Expand All @@ -163,7 +174,7 @@ def create_columns_component_h(data: GTData) -> str:
colspan=len(stub_layout),
style=_flatten_styles(styles_stubhead),
scope="colgroup" if len(stub_layout) > 1 else "col",
id=_process_text_id(stub_label),
id=_create_element_id(table_id, stub_label),
)
)

Expand All @@ -180,7 +191,7 @@ def create_columns_component_h(data: GTData) -> str:
colspan=1,
style=_flatten_styles(styles_column_labels + styles_i),
scope="col",
id=_process_text_id(info.column_label),
id=_create_element_id(table_id, info.var),
)
)

Expand Down Expand Up @@ -225,7 +236,7 @@ def create_columns_component_h(data: GTData) -> str:
colspan=len(stub_layout),
style=_flatten_styles(styles_stubhead),
scope="colgroup" if len(stub_layout) > 1 else "col",
id=_process_text_id(stub_label),
id=_create_element_id(table_id, stub_label),
)
)

Expand Down Expand Up @@ -258,7 +269,7 @@ def create_columns_component_h(data: GTData) -> str:
colspan=1,
style=_flatten_styles(styles_column_labels + styles_i),
scope="col",
id=_process_text_id(h_info.column_label),
id=_create_element_id(table_id, h_info.var),
)
)

Expand Down Expand Up @@ -286,7 +297,7 @@ def create_columns_component_h(data: GTData) -> str:
colspan=colspans[ii],
style=_flatten_styles(styles_column_labels + styles_i),
scope="colgroup" if colspans[ii] > 1 else "col",
id=_process_text_id(spanner_ids_level_1_index[ii]),
id=_create_element_id(table_id, spanner_ids_level_1_index[ii]),
)
)

Expand All @@ -301,6 +312,10 @@ def create_columns_component_h(data: GTData) -> str:
if len(remaining_headings) > 0:
spanned_column_labels = []

remaining_heading_ids = [
entry.var for entry in boxhead if entry.var in remaining_headings
]

for j in range(len(remaining_headings)):
# Filter by column label / id, join with overall column labels style
# TODO check this filter logic
Expand All @@ -318,7 +333,7 @@ def create_columns_component_h(data: GTData) -> str:
colspan=1,
style=_flatten_styles(styles_column_labels + styles_i),
scope="col",
id=_process_text_id(remaining_headings_labels[j]),
id=_create_element_id(table_id, remaining_heading_ids[j]),
)
)

Expand Down
14 changes: 7 additions & 7 deletions tests/__snapshots__/test_export.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id=""></th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="num">num</th>
<th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="char">char</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="currency">currency</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test_table-num">num</th>
<th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="test_table-char">char</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test_table-currency">currency</th>
</tr>
</thead>
<tbody class="gt_table_body">
Expand Down Expand Up @@ -143,8 +143,8 @@
<thead style="border-style: none;">

<tr class="gt_col_headings" style="border-style: none;background-color: transparent;border-top-style: solid;border-top-width: 2px;border-top-color: #D3D3D3;border-bottom-style: solid;border-bottom-width: 2px;border-bottom-color: #D3D3D3;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;">
<th class="gt_col_heading gt_columns_bottom_border gt_right" id="num" rowspan="1" colspan="1" scope="col" style="border-style: none;color: #333333;background-color: #FFFFFF;font-size: 100%;font-weight: normal;text-transform: inherit;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;vertical-align: bottom;padding-top: 5px;padding-bottom: 5px;padding-left: 5px;padding-right: 5px;overflow-x: hidden;text-align: right;font-variant-numeric: tabular-nums;">num</th>
<th class="gt_col_heading gt_columns_bottom_border gt_left" id="char" rowspan="1" colspan="1" scope="col" style="border-style: none;color: #333333;background-color: #FFFFFF;font-size: 100%;font-weight: normal;text-transform: inherit;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;vertical-align: bottom;padding-top: 5px;padding-bottom: 5px;padding-left: 5px;padding-right: 5px;overflow-x: hidden;text-align: left;">char</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" id="test_table_small-num" rowspan="1" colspan="1" scope="col" style="border-style: none;color: #333333;background-color: #FFFFFF;font-size: 100%;font-weight: normal;text-transform: inherit;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;vertical-align: bottom;padding-top: 5px;padding-bottom: 5px;padding-left: 5px;padding-right: 5px;overflow-x: hidden;text-align: right;font-variant-numeric: tabular-nums;">num</th>
<th class="gt_col_heading gt_columns_bottom_border gt_left" id="test_table_small-char" rowspan="1" colspan="1" scope="col" style="border-style: none;color: #333333;background-color: #FFFFFF;font-size: 100%;font-weight: normal;text-transform: inherit;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;vertical-align: bottom;padding-top: 5px;padding-bottom: 5px;padding-left: 5px;padding-right: 5px;overflow-x: hidden;text-align: left;">char</th>
</tr>
</thead>
<tbody class="gt_table_body" style="border-style: none;border-top-style: solid;border-top-width: 2px;border-top-color: #D3D3D3;border-bottom-style: solid;border-bottom-width: 2px;border-bottom-color: #D3D3D3;">
Expand Down Expand Up @@ -176,8 +176,8 @@
<thead style="border-style: none;">

<tr class="gt_col_headings" style="border-style: none;background-color: transparent;border-top-style: solid;border-top-width: 2px;border-top-color: #D3D3D3;border-bottom-style: solid;border-bottom-width: 2px;border-bottom-color: #D3D3D3;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;">
<th class="gt_col_heading gt_columns_bottom_border gt_right" id="num" rowspan="1" colspan="1" scope="col" style="border-style: none;color: #333333;background-color: #FFFFFF;font-size: 100%;font-weight: normal;text-transform: inherit;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;vertical-align: bottom;padding-top: 5px;padding-bottom: 5px;padding-left: 5px;padding-right: 5px;overflow-x: hidden;text-align: right;font-variant-numeric: tabular-nums;">num</th>
<th class="gt_col_heading gt_columns_bottom_border gt_left" id="char" rowspan="1" colspan="1" scope="col" style="border-style: none;color: #333333;background-color: #FFFFFF;font-size: 100%;font-weight: normal;text-transform: inherit;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;vertical-align: bottom;padding-top: 5px;padding-bottom: 5px;padding-left: 5px;padding-right: 5px;overflow-x: hidden;text-align: left;">char</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" id="test_table_small-num" rowspan="1" colspan="1" scope="col" style="border-style: none;color: #333333;background-color: #FFFFFF;font-size: 100%;font-weight: normal;text-transform: inherit;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;vertical-align: bottom;padding-top: 5px;padding-bottom: 5px;padding-left: 5px;padding-right: 5px;overflow-x: hidden;text-align: right;font-variant-numeric: tabular-nums;">num</th>
<th class="gt_col_heading gt_columns_bottom_border gt_left" id="test_table_small-char" rowspan="1" colspan="1" scope="col" style="border-style: none;color: #333333;background-color: #FFFFFF;font-size: 100%;font-weight: normal;text-transform: inherit;border-left-style: none;border-left-width: 1px;border-left-color: #D3D3D3;border-right-style: none;border-right-width: 1px;border-right-color: #D3D3D3;vertical-align: bottom;padding-top: 5px;padding-bottom: 5px;padding-left: 5px;padding-right: 5px;overflow-x: hidden;text-align: left;">char</th>
</tr>
</thead>
<tbody class="gt_table_body" style="border-style: none;border-top-style: solid;border-top-width: 2px;border-top-color: #D3D3D3;border-bottom-style: solid;border-bottom-width: 2px;border-bottom-color: #D3D3D3;">
Expand Down
20 changes: 10 additions & 10 deletions tests/__snapshots__/test_repr.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
<thead>

<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="y">y</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-y">y</th>
</tr>
</thead>
<tbody class="gt_table_body">
Expand Down Expand Up @@ -129,8 +129,8 @@
<thead>

<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="y">y</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-y">y</th>
</tr>
</thead>
<tbody class="gt_table_body">
Expand Down Expand Up @@ -211,8 +211,8 @@
<thead>

<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="y">y</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-y">y</th>
</tr>
</thead>
<tbody class="gt_table_body">
Expand Down Expand Up @@ -290,8 +290,8 @@
<thead>

<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="y">y</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-y">y</th>
</tr>
</thead>
<tbody class="gt_table_body">
Expand Down Expand Up @@ -366,8 +366,8 @@
<thead>

<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="y">y</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-x">x</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test-y">y</th>
</tr>
</thead>
<tbody class="gt_table_body">
Expand Down
8 changes: 8 additions & 0 deletions tests/__snapshots__/test_utils_render_html.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,11 @@
</tbody>
'''
# ---
# name: test_table_id_used_in_headers
'''
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="test_id-Count">Count</th>
<th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="test_id-Group-Label">Group Label</th>
</tr>
'''
# ---
13 changes: 13 additions & 0 deletions tests/test_utils_render_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,16 @@ def test_loc_kitchen_sink(snapshot):
html = new_gt.as_raw_html()
cleaned = html[html.index("<table") :]
assert cleaned == snapshot


def test_table_id_used_in_headers(snapshot):
new_gt = GT(
pl.DataFrame(
{
"Count": [1, 2, 3, 4],
"Group Label": ["label a", "label b", "label c", "label d"],
}
)
).with_id("test_id")

assert_rendered_columns(snapshot, new_gt)

0 comments on commit dc82197

Please sign in to comment.