Add: Coverage gutters
All checks were successful
Python CI / test (push) Successful in 42s

Remove: Un-used funcs
This commit is contained in:
Duncan Tourolle 2025-06-07 17:10:35 +02:00
parent afb22ccb5c
commit ed39f40bad
6 changed files with 4484 additions and 246 deletions

122
COVERAGE_GUTTERS_SETUP.md Normal file
View File

@ -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

4019
coverage.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,20 @@ from typing import List, Dict, Any, Optional, Union, Callable, Tuple, NamedTuple
from bs4 import BeautifulSoup, Tag, NavigableString
from pyWebLayout.abstract.inline import Word, FormattedSpan
from pyWebLayout.abstract.block import (
Block, Paragraph, Heading, HeadingLevel, Quote, CodeBlock,
HList, ListItem, ListStyle, Table, TableRow, TableCell,
HorizontalRule, Image
Block,
Paragraph,
Heading,
HeadingLevel,
Quote,
CodeBlock,
HList,
ListItem,
ListStyle,
Table,
TableRow,
TableCell,
HorizontalRule,
Image,
)
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
@ -23,34 +34,37 @@ class StyleContext(NamedTuple):
Immutable style context passed to handler functions.
Contains all styling information including inherited styles, CSS hints, and element attributes.
"""
font: Font
background: Optional[Tuple[int, int, int, int]]
css_classes: set
css_styles: Dict[str, str]
element_attributes: Dict[str, Any]
parent_elements: List[str] # Stack of parent element names
def with_font(self, font: Font) -> 'StyleContext':
def with_font(self, font: Font) -> "StyleContext":
"""Create new context with modified font."""
return self._replace(font=font)
def with_background(self, background: Optional[Tuple[int, int, int, int]]) -> 'StyleContext':
def with_background(
self, background: Optional[Tuple[int, int, int, int]]
) -> "StyleContext":
"""Create new context with modified background."""
return self._replace(background=background)
def with_css_classes(self, css_classes: set) -> 'StyleContext':
def with_css_classes(self, css_classes: set) -> "StyleContext":
"""Create new context with modified CSS classes."""
return self._replace(css_classes=css_classes)
def with_css_styles(self, css_styles: Dict[str, str]) -> 'StyleContext':
def with_css_styles(self, css_styles: Dict[str, str]) -> "StyleContext":
"""Create new context with modified CSS styles."""
return self._replace(css_styles=css_styles)
def with_attributes(self, attributes: Dict[str, Any]) -> 'StyleContext':
def with_attributes(self, attributes: Dict[str, Any]) -> "StyleContext":
"""Create new context with modified element attributes."""
return self._replace(element_attributes=attributes)
def push_element(self, element_name: str) -> 'StyleContext':
def push_element(self, element_name: str) -> "StyleContext":
"""Create new context with element pushed onto parent stack."""
return self._replace(parent_elements=self.parent_elements + [element_name])
@ -58,10 +72,10 @@ class StyleContext(NamedTuple):
def create_base_context(base_font: Optional[Font] = None) -> StyleContext:
"""
Create a base style context with default values.
Args:
base_font: Base font to use, defaults to system default
Returns:
StyleContext with default values
"""
@ -71,99 +85,105 @@ def create_base_context(base_font: Optional[Font] = None) -> StyleContext:
css_classes=set(),
css_styles={},
element_attributes={},
parent_elements=[]
parent_elements=[],
)
def apply_element_styling(context: StyleContext, element: Tag) -> StyleContext:
"""
Apply element-specific styling to context based on HTML element and attributes.
Args:
context: Current style context
element: BeautifulSoup Tag object
Returns:
New StyleContext with applied styling
"""
tag_name = element.name.lower()
attributes = dict(element.attrs) if element.attrs else {}
# Start with current context
new_context = context.with_attributes(attributes).push_element(tag_name)
# Apply CSS classes
css_classes = new_context.css_classes.copy()
if 'class' in attributes:
classes = attributes['class'].split() if isinstance(attributes['class'], str) else attributes['class']
if "class" in attributes:
classes = (
attributes["class"].split()
if isinstance(attributes["class"], str)
else attributes["class"]
)
css_classes.update(classes)
new_context = new_context.with_css_classes(css_classes)
# Apply inline styles
css_styles = new_context.css_styles.copy()
if 'style' in attributes:
inline_styles = parse_inline_styles(attributes['style'])
if "style" in attributes:
inline_styles = parse_inline_styles(attributes["style"])
css_styles.update(inline_styles)
new_context = new_context.with_css_styles(css_styles)
# Apply element-specific default styles
font = apply_element_font_styles(new_context.font, tag_name, css_styles)
new_context = new_context.with_font(font)
# Apply background from styles
background = apply_background_styles(new_context.background, css_styles)
new_context = new_context.with_background(background)
return new_context
def parse_inline_styles(style_text: str) -> Dict[str, str]:
"""
Parse CSS inline styles into dictionary.
Args:
style_text: CSS style text (e.g., "color: red; font-weight: bold;")
Returns:
Dictionary of CSS property-value pairs
"""
styles = {}
for declaration in style_text.split(';'):
if ':' in declaration:
prop, value = declaration.split(':', 1)
for declaration in style_text.split(";"):
if ":" in declaration:
prop, value = declaration.split(":", 1)
styles[prop.strip().lower()] = value.strip()
return styles
def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, str]) -> Font:
def apply_element_font_styles(
font: Font, tag_name: str, css_styles: Dict[str, str]
) -> Font:
"""
Apply font styling based on HTML element and CSS styles.
Args:
font: Current font
tag_name: HTML tag name
css_styles: CSS styles dictionary
Returns:
New Font object with applied styling
"""
# Default element styles
element_font_styles = {
'b': {'weight': FontWeight.BOLD},
'strong': {'weight': FontWeight.BOLD},
'i': {'style': FontStyle.ITALIC},
'em': {'style': FontStyle.ITALIC},
'u': {'decoration': TextDecoration.UNDERLINE},
's': {'decoration': TextDecoration.STRIKETHROUGH},
'del': {'decoration': TextDecoration.STRIKETHROUGH},
'h1': {'size': 24, 'weight': FontWeight.BOLD},
'h2': {'size': 20, 'weight': FontWeight.BOLD},
'h3': {'size': 18, 'weight': FontWeight.BOLD},
'h4': {'size': 16, 'weight': FontWeight.BOLD},
'h5': {'size': 14, 'weight': FontWeight.BOLD},
'h6': {'size': 12, 'weight': FontWeight.BOLD},
"b": {"weight": FontWeight.BOLD},
"strong": {"weight": FontWeight.BOLD},
"i": {"style": FontStyle.ITALIC},
"em": {"style": FontStyle.ITALIC},
"u": {"decoration": TextDecoration.UNDERLINE},
"s": {"decoration": TextDecoration.STRIKETHROUGH},
"del": {"decoration": TextDecoration.STRIKETHROUGH},
"h1": {"size": 24, "weight": FontWeight.BOLD},
"h2": {"size": 20, "weight": FontWeight.BOLD},
"h3": {"size": 18, "weight": FontWeight.BOLD},
"h4": {"size": 16, "weight": FontWeight.BOLD},
"h5": {"size": 14, "weight": FontWeight.BOLD},
"h6": {"size": 12, "weight": FontWeight.BOLD},
}
# Start with current font properties
font_size = font.font_size
colour = font.colour
@ -172,70 +192,70 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s
decoration = font.decoration
background = font.background
language = font.language
# Apply element default styles
if tag_name in element_font_styles:
elem_styles = element_font_styles[tag_name]
if 'size' in elem_styles:
font_size = elem_styles['size']
if 'weight' in elem_styles:
weight = elem_styles['weight']
if 'style' in elem_styles:
style = elem_styles['style']
if 'decoration' in elem_styles:
decoration = elem_styles['decoration']
if "size" in elem_styles:
font_size = elem_styles["size"]
if "weight" in elem_styles:
weight = elem_styles["weight"]
if "style" in elem_styles:
style = elem_styles["style"]
if "decoration" in elem_styles:
decoration = elem_styles["decoration"]
# Apply CSS styles (override element defaults)
if 'font-size' in css_styles:
if "font-size" in css_styles:
# Parse font-size (simplified - could be enhanced)
size_value = css_styles['font-size'].lower()
if size_value.endswith('px'):
size_value = css_styles["font-size"].lower()
if size_value.endswith("px"):
try:
font_size = int(float(size_value[:-2]))
except ValueError:
pass
elif size_value.endswith('pt'):
elif size_value.endswith("pt"):
try:
font_size = int(float(size_value[:-2]))
except ValueError:
pass
if 'font-weight' in css_styles:
weight_value = css_styles['font-weight'].lower()
if weight_value in ['bold', '700', '800', '900']:
if "font-weight" in css_styles:
weight_value = css_styles["font-weight"].lower()
if weight_value in ["bold", "700", "800", "900"]:
weight = FontWeight.BOLD
elif weight_value in ['normal', '400']:
elif weight_value in ["normal", "400"]:
weight = FontWeight.NORMAL
if 'font-style' in css_styles:
style_value = css_styles['font-style'].lower()
if style_value == 'italic':
if "font-style" in css_styles:
style_value = css_styles["font-style"].lower()
if style_value == "italic":
style = FontStyle.ITALIC
elif style_value == 'normal':
elif style_value == "normal":
style = FontStyle.NORMAL
if 'text-decoration' in css_styles:
decoration_value = css_styles['text-decoration'].lower()
if 'underline' in decoration_value:
if "text-decoration" in css_styles:
decoration_value = css_styles["text-decoration"].lower()
if "underline" in decoration_value:
decoration = TextDecoration.UNDERLINE
elif 'line-through' in decoration_value:
elif "line-through" in decoration_value:
decoration = TextDecoration.STRIKETHROUGH
elif 'none' in decoration_value:
elif "none" in decoration_value:
decoration = TextDecoration.NONE
if 'color' in css_styles:
if "color" in css_styles:
# Parse color (simplified - could be enhanced for hex, rgb, etc.)
color_value = css_styles['color'].lower()
color_value = css_styles["color"].lower()
color_map = {
'black': (0, 0, 0),
'white': (255, 255, 255),
'red': (255, 0, 0),
'green': (0, 255, 0),
'blue': (0, 0, 255),
"black": (0, 0, 0),
"white": (255, 255, 255),
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
}
if color_value in color_map:
colour = color_map[color_value]
elif color_value.startswith('#') and len(color_value) == 7:
elif color_value.startswith("#") and len(color_value) == 7:
try:
r = int(color_value[1:3], 16)
g = int(color_value[3:5], 16)
@ -243,7 +263,7 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s
colour = (r, g, b)
except ValueError:
pass
return Font(
font_path=font._font_path,
font_size=font_size,
@ -252,44 +272,45 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s
style=style,
decoration=decoration,
background=background,
language=language
language=language,
)
def apply_background_styles(current_background: Optional[Tuple[int, int, int, int]],
css_styles: Dict[str, str]) -> Optional[Tuple[int, int, int, int]]:
def apply_background_styles(
current_background: Optional[Tuple[int, int, int, int]], css_styles: Dict[str, str]
) -> Optional[Tuple[int, int, int, int]]:
"""
Apply background styling from CSS.
Args:
current_background: Current background color (RGBA)
css_styles: CSS styles dictionary
Returns:
New background color or None
"""
if 'background-color' in css_styles:
bg_value = css_styles['background-color'].lower()
if bg_value == 'transparent':
if "background-color" in css_styles:
bg_value = css_styles["background-color"].lower()
if bg_value == "transparent":
return None
# Add color parsing logic here if needed
return current_background
def extract_text_content(element: Tag, context: StyleContext) -> List[Word]:
"""
Extract text content from an element, handling inline formatting.
Args:
element: BeautifulSoup Tag object
context: Current style context
Returns:
List of Word objects
"""
words = []
for child in element.children:
if isinstance(child, NavigableString):
# Plain text - split into words
@ -301,7 +322,27 @@ def extract_text_content(element: Tag, context: StyleContext) -> List[Word]:
words.append(Word(word_text, context.font, context.background))
elif isinstance(child, Tag):
# Process inline elements
if child.name.lower() in ['span', 'a', 'strong', 'b', 'em', 'i', 'u', 's', 'del', 'ins', 'mark', 'small', 'sub', 'sup', 'code', 'q', 'cite', 'abbr', 'time']:
if child.name.lower() in [
"span",
"a",
"strong",
"b",
"em",
"i",
"u",
"s",
"del",
"ins",
"mark",
"small",
"sub",
"sup",
"code",
"q",
"cite",
"abbr",
"time",
]:
child_context = apply_element_styling(context, child)
child_words = extract_text_content(child, child_context)
words.extend(child_words)
@ -317,18 +358,20 @@ def extract_text_content(element: Tag, context: StyleContext) -> List[Word]:
elif isinstance(child_result, Paragraph):
for _, word in child_result.words():
words.append(word)
return words
def process_element(element: Tag, context: StyleContext) -> Union[Block, List[Block], None]:
def process_element(
element: Tag, context: StyleContext
) -> Union[Block, List[Block], None]:
"""
Process a single HTML element using appropriate handler.
Args:
element: BeautifulSoup Tag object
context: Current style context
Returns:
Block object(s) or None if element should be ignored
"""
@ -340,6 +383,7 @@ def process_element(element: Tag, context: StyleContext) -> Union[Block, List[Bl
# Handler function signatures:
# All handlers receive (element: Tag, context: StyleContext) -> Union[Block, List[Block], None]
def paragraph_handler(element: Tag, context: StyleContext) -> Paragraph:
"""Handle <p> 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 <h1>-<h6> elements."""
level_map = {
'h1': HeadingLevel.H1,
'h2': HeadingLevel.H2,
'h3': HeadingLevel.H3,
'h4': HeadingLevel.H4,
'h5': HeadingLevel.H5,
'h6': HeadingLevel.H6,
"h1": HeadingLevel.H1,
"h2": HeadingLevel.H2,
"h3": HeadingLevel.H3,
"h4": HeadingLevel.H4,
"h5": HeadingLevel.H5,
"h6": HeadingLevel.H6,
}
level = level_map.get(element.name.lower(), HeadingLevel.H1)
heading = Heading(level, context.font)
words = extract_text_content(element, context)
@ -401,23 +445,23 @@ def blockquote_handler(element: Tag, context: StyleContext) -> Quote:
def preformatted_handler(element: Tag, context: StyleContext) -> CodeBlock:
"""Handle <pre> 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 <code> elements."""
# If parent is <pre>, 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 <ul> 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 <ol> 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 <li> 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 <table> 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 <tr> 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 <td> 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 <th> 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 <img> 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)

View File

@ -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"

48
run_coverage_gutters.py Normal file
View File

@ -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()

View File

@ -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}")