Remove: Un-used funcs
This commit is contained in:
parent
afb22ccb5c
commit
ed39f40bad
122
COVERAGE_GUTTERS_SETUP.md
Normal file
122
COVERAGE_GUTTERS_SETUP.md
Normal 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
4019
coverage.xml
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
||||
@ -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
48
run_coverage_gutters.py
Normal 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()
|
||||
@ -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}")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user