Skip to content

Commit 98e6162

Browse files
authored
Merge pull request #62 from MotleyAI/egor/add_to_html_method_for_tables
Add to_html method for TableData
2 parents c772530 + 65f1922 commit 98e6162

3 files changed

Lines changed: 98 additions & 2 deletions

File tree

gslides_api/agnostic/element.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import html
12
import json
23
import re
34
from abc import ABC, abstractmethod
45
from enum import Enum
5-
from typing import Any, List, Literal
6+
from typing import Any, List, Literal, Optional
67

78
import marko
89
from pydantic import BaseModel, Field, field_validator, model_validator
@@ -53,6 +54,32 @@ def to_markdown(self) -> str:
5354

5455
return "\n".join(lines)
5556

57+
def to_html(self, css_class: Optional[str] = None) -> str:
58+
"""Convert table data to an HTML <table> element.
59+
60+
Args:
61+
css_class: Optional CSS class to apply to the <table> element.
62+
63+
Returns:
64+
HTML string with <table>, <thead>, and <tbody> elements.
65+
"""
66+
if not self.headers:
67+
return ""
68+
69+
cls_attr = f' class="{html.escape(css_class)}"' if css_class else ""
70+
parts = [f"<table{cls_attr}>", "<thead><tr>"]
71+
for header in self.headers:
72+
parts.append(f"<th>{html.escape(str(header))}</th>")
73+
parts.append("</tr></thead><tbody>")
74+
for row in self.rows:
75+
parts.append("<tr>")
76+
for i in range(len(self.headers)):
77+
cell = str(row[i]) if i < len(row) else ""
78+
parts.append(f"<td>{html.escape(cell)}</td>")
79+
parts.append("</tr>")
80+
parts.append("</tbody></table>")
81+
return "".join(parts)
82+
5683
def to_dataframe(self):
5784
"""Convert table data to pandas DataFrame.
5885

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "gslides-api"
3-
version = "0.3.5"
3+
version = "0.3.6"
44
description = "A Python library for working with Google Slides API using Pydantic domain objects"
55
authors = ["motley.ai <info@motley.ai>"]
66
license = "MIT"

tests/test_table_data_to_html.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Tests for TableData.to_html() method."""
2+
3+
import pytest
4+
5+
from gslides_api.agnostic.element import TableData
6+
7+
8+
class TestTableDataToHtml:
9+
def test_basic_table(self):
10+
td = TableData(headers=["Name", "Value"], rows=[["Alice", "100"], ["Bob", "200"]])
11+
result = td.to_html()
12+
assert "<table>" in result
13+
assert "<thead>" in result
14+
assert "<tbody>" in result
15+
assert "<th>Name</th>" in result
16+
assert "<th>Value</th>" in result
17+
assert "<td>Alice</td>" in result
18+
assert "<td>100</td>" in result
19+
assert "<td>Bob</td>" in result
20+
assert "<td>200</td>" in result
21+
22+
def test_with_css_class(self):
23+
td = TableData(headers=["A"], rows=[["1"]])
24+
result = td.to_html(css_class="dtbl")
25+
assert '<table class="dtbl">' in result
26+
27+
def test_without_css_class(self):
28+
td = TableData(headers=["A"], rows=[["1"]])
29+
result = td.to_html()
30+
assert "<table>" in result
31+
assert "class=" not in result
32+
33+
def test_empty_headers(self):
34+
td = TableData(headers=[], rows=[])
35+
assert td.to_html() == ""
36+
37+
def test_html_escaping(self):
38+
td = TableData(
39+
headers=["<script>", "A&B"],
40+
rows=[["x < y", '"quoted"']],
41+
)
42+
result = td.to_html()
43+
assert "&lt;script&gt;" in result
44+
assert "A&amp;B" in result
45+
assert "x &lt; y" in result
46+
assert "&quot;quoted&quot;" in result
47+
assert "<script>" not in result # must be escaped
48+
49+
def test_css_class_escaping(self):
50+
td = TableData(headers=["A"], rows=[["1"]])
51+
result = td.to_html(css_class='x" onclick="alert(1)')
52+
# The double quote must be escaped so it can't break out of the attribute
53+
assert '&quot;' in result
54+
assert 'class="x&quot; onclick=&quot;alert(1)"' in result
55+
56+
def test_short_row_pads_with_empty(self):
57+
td = TableData(headers=["A", "B", "C"], rows=[["only_one"]])
58+
result = td.to_html()
59+
assert "<td>only_one</td>" in result
60+
assert result.count("<td></td>") == 2
61+
62+
def test_structure_order(self):
63+
td = TableData(headers=["H"], rows=[["R"]])
64+
result = td.to_html()
65+
# Verify structural ordering
66+
assert result.index("<thead>") < result.index("</thead>")
67+
assert result.index("</thead>") < result.index("<tbody>")
68+
assert result.index("<tbody>") < result.index("</tbody>")
69+
assert result.index("</tbody>") < result.index("</table>")

0 commit comments

Comments
 (0)