diff --git a/COVERAGE_GUTTERS_SETUP.md b/COVERAGE_GUTTERS_SETUP.md
new file mode 100644
index 0000000..f037e49
--- /dev/null
+++ b/COVERAGE_GUTTERS_SETUP.md
@@ -0,0 +1,122 @@
+# Coverage Gutters Setup Guide
+
+This guide helps you set up Coverage Gutters in VSCode to visualize code coverage for the pyWebLayout project.
+
+## Prerequisites
+
+1. **Install VSCode Extension**: Make sure you have the "Coverage Gutters" extension installed in VSCode.
+2. **Install Python packages**:
+ ```bash
+ pip install pytest pytest-cov coverage
+ ```
+
+## Configuration Files
+
+### 1. VSCode Settings (`.vscode/settings.json`)
+
+Your VSCode settings are already configured with:
+```json
+{
+ "coverage-gutters.coverageBaseDir": "./",
+ "coverage-gutters.coverageFileNames": [
+ "coverage.xml",
+ "lcov.info",
+ "cov.xml",
+ "coverage.info"
+ ],
+ "coverage-gutters.showGutterCoverage": true,
+ "coverage-gutters.showLineCoverage": true,
+ "coverage-gutters.showRulerCoverage": true,
+ "coverage-gutters.xmlname": "coverage.xml"
+}
+```
+
+### 2. Coverage Configuration (`pyproject.toml`)
+
+Coverage settings are configured in `pyproject.toml`:
+```toml
+[tool.coverage.run]
+source = ["pyWebLayout"]
+branch = true
+omit = [
+ "*/tests/*",
+ "*/test_*",
+ "setup.py",
+ "*/examples/*",
+ "*/__main__.py"
+]
+
+[tool.coverage.xml]
+output = "coverage.xml"
+
+[tool.coverage.html]
+directory = "htmlcov"
+```
+
+## How to Generate Coverage
+
+### Option 1: Quick Coverage for Gutters
+```bash
+python run_coverage_gutters.py
+```
+
+### Option 2: Manual pytest command
+```bash
+python -m pytest tests/ --cov=pyWebLayout --cov-report=xml --cov-report=html --cov-report=term
+```
+
+### Option 3: Full coverage analysis
+```bash
+python scripts/run_coverage.py
+```
+
+## Using Coverage Gutters in VSCode
+
+1. **Generate coverage data** using one of the options above
+2. **Open VSCode** and navigate to your Python source files
+3. **Enable Coverage Gutters**:
+ - Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac)
+ - Type "Coverage Gutters: Display Coverage"
+ - Or click the "Watch" button in the status bar
+
+## What You'll See
+
+- **Green lines**: Code that is covered by tests
+- **Red lines**: Code that is NOT covered by tests
+- **Yellow lines**: Partially covered code (branches)
+- **Coverage percentage** in the status bar
+
+## Troubleshooting
+
+1. **No coverage showing**:
+ - Make sure `coverage.xml` exists in the project root
+ - Check that the Coverage Gutters extension is enabled
+ - Try reloading VSCode window
+
+2. **Coverage not updating**:
+ - Re-run the coverage command
+ - Click "Watch" in the status bar to refresh
+
+3. **Tests not running**:
+ - Make sure you're in the project root directory
+ - Install missing dependencies: `pip install pytest pytest-cov`
+
+## Coverage Files Generated
+
+After running coverage, you should see:
+- `coverage.xml` - XML format for Coverage Gutters
+- `htmlcov/` - HTML coverage report directory
+- `coverage.json` - JSON format for badges
+- `.coverage` - Coverage database file
+
+## Commands Summary
+
+```bash
+# Install dependencies
+pip install pytest pytest-cov coverage
+
+# Generate coverage for gutters
+python run_coverage_gutters.py
+
+# View HTML report
+open htmlcov/index.html
diff --git a/coverage.xml b/coverage.xml
new file mode 100644
index 0000000..ab2f637
--- /dev/null
+++ b/coverage.xml
@@ -0,0 +1,4019 @@
+
+
elements.""" paragraph = Paragraph(context.font) @@ -367,14 +411,14 @@ def div_handler(element: Tag, context: StyleContext) -> List[Block]: def heading_handler(element: Tag, context: StyleContext) -> Heading: """Handle
elements."""
- language = context.element_attributes.get('data-language', '')
+ language = context.element_attributes.get("data-language", "")
code_block = CodeBlock(language)
-
+
# Preserve whitespace and line breaks in preformatted text
- text = element.get_text(separator='\n', strip=False)
- for line in text.split('\n'):
+ text = element.get_text(separator="\n", strip=False)
+ for line in text.split("\n"):
code_block.add_line(line)
-
+
return code_block
def code_handler(element: Tag, context: StyleContext) -> Union[CodeBlock, None]:
"""Handle elements."""
# If parent is , this is handled by preformatted_handler
- if context.parent_elements and context.parent_elements[-1] == 'pre':
+ if context.parent_elements and context.parent_elements[-1] == "pre":
return None # Will be handled by parent
-
+
# Inline code - handled during text extraction
return None
@@ -426,7 +470,7 @@ def unordered_list_handler(element: Tag, context: StyleContext) -> HList:
"""Handle elements."""
hlist = HList(ListStyle.UNORDERED, context.font)
for child in element.children:
- if isinstance(child, Tag) and child.name.lower() == 'li':
+ if isinstance(child, Tag) and child.name.lower() == "li":
child_context = apply_element_styling(context, child)
item = process_element(child, child_context)
if item:
@@ -438,7 +482,7 @@ def ordered_list_handler(element: Tag, context: StyleContext) -> HList:
"""Handle elements."""
hlist = HList(ListStyle.ORDERED, context.font)
for child in element.children:
- if isinstance(child, Tag) and child.name.lower() == 'li':
+ if isinstance(child, Tag) and child.name.lower() == "li":
child_context = apply_element_styling(context, child)
item = process_element(child, child_context)
if item:
@@ -449,7 +493,7 @@ def ordered_list_handler(element: Tag, context: StyleContext) -> HList:
def list_item_handler(element: Tag, context: StyleContext) -> ListItem:
"""Handle - elements."""
list_item = ListItem(None, context.font)
-
+
for child in element.children:
if isinstance(child, Tag):
child_context = apply_element_styling(context, child)
@@ -470,37 +514,37 @@ def list_item_handler(element: Tag, context: StyleContext) -> ListItem:
if word_text:
paragraph.add_word(Word(word_text, context.font))
list_item.add_block(paragraph)
-
+
return list_item
def table_handler(element: Tag, context: StyleContext) -> Table:
"""Handle
elements."""
caption = None
- caption_elem = element.find('caption')
+ caption_elem = element.find("caption")
if caption_elem:
caption = caption_elem.get_text(strip=True)
-
+
table = Table(caption, context.font)
-
+
# Process table rows
for child in element.children:
if isinstance(child, Tag):
- if child.name.lower() == 'tr':
+ if child.name.lower() == "tr":
child_context = apply_element_styling(context, child)
row = process_element(child, child_context)
if row:
table.add_row(row)
- elif child.name.lower() in ['thead', 'tbody', 'tfoot']:
- section = 'header' if child.name.lower() == 'thead' else 'body'
- section = 'footer' if child.name.lower() == 'tfoot' else section
-
- for row_elem in child.find_all('tr'):
+ elif child.name.lower() in ["thead", "tbody", "tfoot"]:
+ section = "header" if child.name.lower() == "thead" else "body"
+ section = "footer" if child.name.lower() == "tfoot" else section
+
+ for row_elem in child.find_all("tr"):
child_context = apply_element_styling(context, row_elem)
row = process_element(row_elem, child_context)
if row:
table.add_row(row, section)
-
+
return table
@@ -508,7 +552,7 @@ def table_row_handler(element: Tag, context: StyleContext) -> TableRow:
"""Handle elements."""
row = TableRow(context.font)
for child in element.children:
- if isinstance(child, Tag) and child.name.lower() in ['td', 'th']:
+ if isinstance(child, Tag) and child.name.lower() in ["td", "th"]:
child_context = apply_element_styling(context, child)
cell = process_element(child, child_context)
if cell:
@@ -518,10 +562,10 @@ def table_row_handler(element: Tag, context: StyleContext) -> TableRow:
def table_cell_handler(element: Tag, context: StyleContext) -> TableCell:
"""Handle elements."""
- colspan = int(context.element_attributes.get('colspan', 1))
- rowspan = int(context.element_attributes.get('rowspan', 1))
+ colspan = int(context.element_attributes.get("colspan", 1))
+ rowspan = int(context.element_attributes.get("rowspan", 1))
cell = TableCell(False, colspan, rowspan, context.font)
-
+
# Process cell content
for child in element.children:
if isinstance(child, Tag):
@@ -543,16 +587,16 @@ def table_cell_handler(element: Tag, context: StyleContext) -> TableCell:
if word_text:
paragraph.add_word(Word(word_text, context.font))
cell.add_block(paragraph)
-
+
return cell
def table_header_cell_handler(element: Tag, context: StyleContext) -> TableCell:
"""Handle elements."""
- colspan = int(context.element_attributes.get('colspan', 1))
- rowspan = int(context.element_attributes.get('rowspan', 1))
+ colspan = int(context.element_attributes.get("colspan", 1))
+ rowspan = int(context.element_attributes.get("rowspan", 1))
cell = TableCell(True, colspan, rowspan, context.font)
-
+
# Process cell content (same as td)
for child in element.children:
if isinstance(child, Tag):
@@ -573,7 +617,7 @@ def table_header_cell_handler(element: Tag, context: StyleContext) -> TableCell:
if word_text:
paragraph.add_word(Word(word_text, context.font))
cell.add_block(paragraph)
-
+
return cell
@@ -590,19 +634,19 @@ def line_break_handler(element: Tag, context: StyleContext) -> None:
def image_handler(element: Tag, context: StyleContext) -> Image:
"""Handle
elements."""
- src = context.element_attributes.get('src', '')
- alt_text = context.element_attributes.get('alt', '')
-
+ src = context.element_attributes.get("src", "")
+ alt_text = context.element_attributes.get("alt", "")
+
# Parse dimensions if provided
width = height = None
try:
- if 'width' in context.element_attributes:
- width = int(context.element_attributes['width'])
- if 'height' in context.element_attributes:
- height = int(context.element_attributes['height'])
+ if "width" in context.element_attributes:
+ width = int(context.element_attributes["width"])
+ if "height" in context.element_attributes:
+ height = int(context.element_attributes["height"])
except ValueError:
pass
-
+
return Image(source=src, alt_text=alt_text, width=width, height=height)
@@ -619,89 +663,87 @@ def generic_handler(element: Tag, context: StyleContext) -> List[Block]:
# Handler registry - maps HTML tag names to handler functions
HANDLERS: Dict[str, Callable[[Tag, StyleContext], Union[Block, List[Block], None]]] = {
# Block elements
- 'p': paragraph_handler,
- 'div': div_handler,
- 'h1': heading_handler,
- 'h2': heading_handler,
- 'h3': heading_handler,
- 'h4': heading_handler,
- 'h5': heading_handler,
- 'h6': heading_handler,
- 'blockquote': blockquote_handler,
- 'pre': preformatted_handler,
- 'code': code_handler,
- 'ul': unordered_list_handler,
- 'ol': ordered_list_handler,
- 'li': list_item_handler,
- 'table': table_handler,
- 'tr': table_row_handler,
- 'td': table_cell_handler,
- 'th': table_header_cell_handler,
- 'hr': horizontal_rule_handler,
- 'br': line_break_handler,
-
+ "p": paragraph_handler,
+ "div": div_handler,
+ "h1": heading_handler,
+ "h2": heading_handler,
+ "h3": heading_handler,
+ "h4": heading_handler,
+ "h5": heading_handler,
+ "h6": heading_handler,
+ "blockquote": blockquote_handler,
+ "pre": preformatted_handler,
+ "code": code_handler,
+ "ul": unordered_list_handler,
+ "ol": ordered_list_handler,
+ "li": list_item_handler,
+ "table": table_handler,
+ "tr": table_row_handler,
+ "td": table_cell_handler,
+ "th": table_header_cell_handler,
+ "hr": horizontal_rule_handler,
+ "br": line_break_handler,
# Semantic elements (treated as containers)
- 'section': div_handler,
- 'article': div_handler,
- 'aside': div_handler,
- 'nav': div_handler,
- 'header': div_handler,
- 'footer': div_handler,
- 'main': div_handler,
- 'figure': div_handler,
- 'figcaption': paragraph_handler,
-
+ "section": div_handler,
+ "article": div_handler,
+ "aside": div_handler,
+ "nav": div_handler,
+ "header": div_handler,
+ "footer": div_handler,
+ "main": div_handler,
+ "figure": div_handler,
+ "figcaption": paragraph_handler,
# Media elements
- 'img': image_handler,
-
+ "img": image_handler,
# Inline elements (handled during text extraction)
- 'span': ignore_handler,
- 'a': ignore_handler,
- 'strong': ignore_handler,
- 'b': ignore_handler,
- 'em': ignore_handler,
- 'i': ignore_handler,
- 'u': ignore_handler,
- 's': ignore_handler,
- 'del': ignore_handler,
- 'ins': ignore_handler,
- 'mark': ignore_handler,
- 'small': ignore_handler,
- 'sub': ignore_handler,
- 'sup': ignore_handler,
- 'q': ignore_handler,
- 'cite': ignore_handler,
- 'abbr': ignore_handler,
- 'time': ignore_handler,
-
+ "span": ignore_handler,
+ "a": ignore_handler,
+ "strong": ignore_handler,
+ "b": ignore_handler,
+ "em": ignore_handler,
+ "i": ignore_handler,
+ "u": ignore_handler,
+ "s": ignore_handler,
+ "del": ignore_handler,
+ "ins": ignore_handler,
+ "mark": ignore_handler,
+ "small": ignore_handler,
+ "sub": ignore_handler,
+ "sup": ignore_handler,
+ "q": ignore_handler,
+ "cite": ignore_handler,
+ "abbr": ignore_handler,
+ "time": ignore_handler,
# Ignored elements
- 'script': ignore_handler,
- 'style': ignore_handler,
- 'meta': ignore_handler,
- 'link': ignore_handler,
- 'head': ignore_handler,
- 'title': ignore_handler,
+ "script": ignore_handler,
+ "style": ignore_handler,
+ "meta": ignore_handler,
+ "link": ignore_handler,
+ "head": ignore_handler,
+ "title": ignore_handler,
}
-def parse_html_string(html_string: str, base_font: Optional[Font] = None) -> List[Block]:
+def parse_html_string(
+ html_string: str, base_font: Optional[Font] = None
+) -> List[Block]:
"""
Parse HTML string and return list of Block objects.
-
+
Args:
html_string: HTML content to parse
base_font: Base font for styling, defaults to system default
-
+
Returns:
List of Block objects representing the document structure
"""
- soup = BeautifulSoup(html_string, 'html.parser')
+ soup = BeautifulSoup(html_string, "html.parser")
context = create_base_context(base_font)
blocks = []
-
+
# Process the body if it exists, otherwise process all top-level elements
- root_element = soup.find('body') or soup
-
+ root_element = soup.find("body") or soup
+
for element in root_element.children:
if isinstance(element, Tag):
element_context = apply_element_styling(context, element)
@@ -711,29 +753,5 @@ def parse_html_string(html_string: str, base_font: Optional[Font] = None) -> Lis
blocks.extend(result)
else:
blocks.append(result)
-
+
return blocks
-
-
-def register_handler(tag_name: str, handler: Callable[[Tag, StyleContext], Union[Block, List[Block], None]]):
- """
- Register a custom handler for an HTML tag.
-
- Args:
- tag_name: HTML tag name (lowercase)
- handler: Handler function with signature (element: Tag, context: StyleContext) -> Union[Block, List[Block], None]
- """
- HANDLERS[tag_name] = handler
-
-
-def get_handler(tag_name: str) -> Callable[[Tag, StyleContext], Union[Block, List[Block], None]]:
- """
- Get handler function for HTML tag.
-
- Args:
- tag_name: HTML tag name (lowercase)
-
- Returns:
- Handler function or generic_handler if tag not found
- """
- return HANDLERS.get(tag_name.lower(), generic_handler)
diff --git a/pyproject.toml b/pyproject.toml
index 5d070b3..561126a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,3 +18,34 @@ dependencies = [
"pyphen",
"beautifulsoup4",
]
+
+[tool.coverage.run]
+source = ["pyWebLayout"]
+branch = true
+omit = [
+ "*/tests/*",
+ "*/test_*",
+ "setup.py",
+ "*/examples/*",
+ "*/__main__.py"
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "pragma: no cover",
+ "def __repr__",
+ "if self.debug:",
+ "if settings.DEBUG",
+ "raise AssertionError",
+ "raise NotImplementedError",
+ "if 0:",
+ "if __name__ == .__main__.:"
+]
+precision = 2
+show_missing = true
+
+[tool.coverage.xml]
+output = "coverage.xml"
+
+[tool.coverage.html]
+directory = "htmlcov"
diff --git a/run_coverage_gutters.py b/run_coverage_gutters.py
new file mode 100644
index 0000000..e8b5990
--- /dev/null
+++ b/run_coverage_gutters.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+"""
+Simple coverage runner for Coverage Gutters extension.
+Generates coverage.xml file needed by the VSCode Coverage Gutters extension.
+"""
+
+import subprocess
+import sys
+import os
+
+
+def main():
+ """Run coverage for Coverage Gutters."""
+ print("Generating coverage for Coverage Gutters...")
+
+ try:
+ # Run tests with coverage and generate XML report
+ cmd = [
+ sys.executable, "-m", "pytest",
+ "tests/",
+ "--cov=pyWebLayout",
+ "--cov-report=xml",
+ "--cov-report=term"
+ ]
+
+ result = subprocess.run(cmd, check=True)
+
+ # Check if coverage.xml was created
+ if os.path.exists("coverage.xml"):
+ print("✓ coverage.xml generated successfully!")
+ print("Coverage Gutters should now be able to display coverage data.")
+ print("\nTo use Coverage Gutters in VSCode:")
+ print("1. Open Command Palette (Ctrl+Shift+P)")
+ print("2. Run 'Coverage Gutters: Display Coverage'")
+ print("3. Or use the Coverage Gutters buttons in the status bar")
+ else:
+ print("✗ coverage.xml was not generated")
+
+ except subprocess.CalledProcessError as e:
+ print(f"Error running tests: {e}")
+ sys.exit(1)
+ except FileNotFoundError:
+ print("pytest not found. Please install it with: pip install pytest pytest-cov")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/run_coverage.py b/scripts/run_coverage.py
index 52cfb94..64fd701 100644
--- a/scripts/run_coverage.py
+++ b/scripts/run_coverage.py
@@ -53,7 +53,7 @@ def main():
# Run tests with coverage
print("\n2. Running tests with coverage...")
- test_cmd = "python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing --cov-report=json --cov-report=html"
+ test_cmd = "python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing --cov-report=json --cov-report=html --cov-report=xml"
run_command(test_cmd, "Running tests with coverage")
# Generate test coverage badge
@@ -89,7 +89,7 @@ else:
# List generated files
print("\n6. Generated files:")
- files = ["coverage.svg", "coverage-docs.svg", "coverage-summary.txt", "htmlcov/", "coverage.json"]
+ files = ["coverage.svg", "coverage-docs.svg", "coverage-summary.txt", "htmlcov/", "coverage.json", "coverage.xml"]
for file in files:
if os.path.exists(file):
print(f" ✓ {file}")