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 bs4 import BeautifulSoup, Tag, NavigableString
|
||||||
from pyWebLayout.abstract.inline import Word, FormattedSpan
|
from pyWebLayout.abstract.inline import Word, FormattedSpan
|
||||||
from pyWebLayout.abstract.block import (
|
from pyWebLayout.abstract.block import (
|
||||||
Block, Paragraph, Heading, HeadingLevel, Quote, CodeBlock,
|
Block,
|
||||||
HList, ListItem, ListStyle, Table, TableRow, TableCell,
|
Paragraph,
|
||||||
HorizontalRule, Image
|
Heading,
|
||||||
|
HeadingLevel,
|
||||||
|
Quote,
|
||||||
|
CodeBlock,
|
||||||
|
HList,
|
||||||
|
ListItem,
|
||||||
|
ListStyle,
|
||||||
|
Table,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
HorizontalRule,
|
||||||
|
Image,
|
||||||
)
|
)
|
||||||
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||||
|
|
||||||
@ -23,6 +34,7 @@ class StyleContext(NamedTuple):
|
|||||||
Immutable style context passed to handler functions.
|
Immutable style context passed to handler functions.
|
||||||
Contains all styling information including inherited styles, CSS hints, and element attributes.
|
Contains all styling information including inherited styles, CSS hints, and element attributes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
font: Font
|
font: Font
|
||||||
background: Optional[Tuple[int, int, int, int]]
|
background: Optional[Tuple[int, int, int, int]]
|
||||||
css_classes: set
|
css_classes: set
|
||||||
@ -30,27 +42,29 @@ class StyleContext(NamedTuple):
|
|||||||
element_attributes: Dict[str, Any]
|
element_attributes: Dict[str, Any]
|
||||||
parent_elements: List[str] # Stack of parent element names
|
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."""
|
"""Create new context with modified font."""
|
||||||
return self._replace(font=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."""
|
"""Create new context with modified background."""
|
||||||
return self._replace(background=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."""
|
"""Create new context with modified CSS classes."""
|
||||||
return self._replace(css_classes=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."""
|
"""Create new context with modified CSS styles."""
|
||||||
return self._replace(css_styles=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."""
|
"""Create new context with modified element attributes."""
|
||||||
return self._replace(element_attributes=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."""
|
"""Create new context with element pushed onto parent stack."""
|
||||||
return self._replace(parent_elements=self.parent_elements + [element_name])
|
return self._replace(parent_elements=self.parent_elements + [element_name])
|
||||||
|
|
||||||
@ -71,7 +85,7 @@ def create_base_context(base_font: Optional[Font] = None) -> StyleContext:
|
|||||||
css_classes=set(),
|
css_classes=set(),
|
||||||
css_styles={},
|
css_styles={},
|
||||||
element_attributes={},
|
element_attributes={},
|
||||||
parent_elements=[]
|
parent_elements=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -94,15 +108,19 @@ def apply_element_styling(context: StyleContext, element: Tag) -> StyleContext:
|
|||||||
|
|
||||||
# Apply CSS classes
|
# Apply CSS classes
|
||||||
css_classes = new_context.css_classes.copy()
|
css_classes = new_context.css_classes.copy()
|
||||||
if 'class' in attributes:
|
if "class" in attributes:
|
||||||
classes = attributes['class'].split() if isinstance(attributes['class'], str) else attributes['class']
|
classes = (
|
||||||
|
attributes["class"].split()
|
||||||
|
if isinstance(attributes["class"], str)
|
||||||
|
else attributes["class"]
|
||||||
|
)
|
||||||
css_classes.update(classes)
|
css_classes.update(classes)
|
||||||
new_context = new_context.with_css_classes(css_classes)
|
new_context = new_context.with_css_classes(css_classes)
|
||||||
|
|
||||||
# Apply inline styles
|
# Apply inline styles
|
||||||
css_styles = new_context.css_styles.copy()
|
css_styles = new_context.css_styles.copy()
|
||||||
if 'style' in attributes:
|
if "style" in attributes:
|
||||||
inline_styles = parse_inline_styles(attributes['style'])
|
inline_styles = parse_inline_styles(attributes["style"])
|
||||||
css_styles.update(inline_styles)
|
css_styles.update(inline_styles)
|
||||||
new_context = new_context.with_css_styles(css_styles)
|
new_context = new_context.with_css_styles(css_styles)
|
||||||
|
|
||||||
@ -128,14 +146,16 @@ def parse_inline_styles(style_text: str) -> Dict[str, str]:
|
|||||||
Dictionary of CSS property-value pairs
|
Dictionary of CSS property-value pairs
|
||||||
"""
|
"""
|
||||||
styles = {}
|
styles = {}
|
||||||
for declaration in style_text.split(';'):
|
for declaration in style_text.split(";"):
|
||||||
if ':' in declaration:
|
if ":" in declaration:
|
||||||
prop, value = declaration.split(':', 1)
|
prop, value = declaration.split(":", 1)
|
||||||
styles[prop.strip().lower()] = value.strip()
|
styles[prop.strip().lower()] = value.strip()
|
||||||
return styles
|
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.
|
Apply font styling based on HTML element and CSS styles.
|
||||||
|
|
||||||
@ -149,19 +169,19 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s
|
|||||||
"""
|
"""
|
||||||
# Default element styles
|
# Default element styles
|
||||||
element_font_styles = {
|
element_font_styles = {
|
||||||
'b': {'weight': FontWeight.BOLD},
|
"b": {"weight": FontWeight.BOLD},
|
||||||
'strong': {'weight': FontWeight.BOLD},
|
"strong": {"weight": FontWeight.BOLD},
|
||||||
'i': {'style': FontStyle.ITALIC},
|
"i": {"style": FontStyle.ITALIC},
|
||||||
'em': {'style': FontStyle.ITALIC},
|
"em": {"style": FontStyle.ITALIC},
|
||||||
'u': {'decoration': TextDecoration.UNDERLINE},
|
"u": {"decoration": TextDecoration.UNDERLINE},
|
||||||
's': {'decoration': TextDecoration.STRIKETHROUGH},
|
"s": {"decoration": TextDecoration.STRIKETHROUGH},
|
||||||
'del': {'decoration': TextDecoration.STRIKETHROUGH},
|
"del": {"decoration": TextDecoration.STRIKETHROUGH},
|
||||||
'h1': {'size': 24, 'weight': FontWeight.BOLD},
|
"h1": {"size": 24, "weight": FontWeight.BOLD},
|
||||||
'h2': {'size': 20, 'weight': FontWeight.BOLD},
|
"h2": {"size": 20, "weight": FontWeight.BOLD},
|
||||||
'h3': {'size': 18, 'weight': FontWeight.BOLD},
|
"h3": {"size": 18, "weight": FontWeight.BOLD},
|
||||||
'h4': {'size': 16, 'weight': FontWeight.BOLD},
|
"h4": {"size": 16, "weight": FontWeight.BOLD},
|
||||||
'h5': {'size': 14, 'weight': FontWeight.BOLD},
|
"h5": {"size": 14, "weight": FontWeight.BOLD},
|
||||||
'h6': {'size': 12, 'weight': FontWeight.BOLD},
|
"h6": {"size": 12, "weight": FontWeight.BOLD},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Start with current font properties
|
# Start with current font properties
|
||||||
@ -176,66 +196,66 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s
|
|||||||
# Apply element default styles
|
# Apply element default styles
|
||||||
if tag_name in element_font_styles:
|
if tag_name in element_font_styles:
|
||||||
elem_styles = element_font_styles[tag_name]
|
elem_styles = element_font_styles[tag_name]
|
||||||
if 'size' in elem_styles:
|
if "size" in elem_styles:
|
||||||
font_size = elem_styles['size']
|
font_size = elem_styles["size"]
|
||||||
if 'weight' in elem_styles:
|
if "weight" in elem_styles:
|
||||||
weight = elem_styles['weight']
|
weight = elem_styles["weight"]
|
||||||
if 'style' in elem_styles:
|
if "style" in elem_styles:
|
||||||
style = elem_styles['style']
|
style = elem_styles["style"]
|
||||||
if 'decoration' in elem_styles:
|
if "decoration" in elem_styles:
|
||||||
decoration = elem_styles['decoration']
|
decoration = elem_styles["decoration"]
|
||||||
|
|
||||||
# Apply CSS styles (override element defaults)
|
# 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)
|
# Parse font-size (simplified - could be enhanced)
|
||||||
size_value = css_styles['font-size'].lower()
|
size_value = css_styles["font-size"].lower()
|
||||||
if size_value.endswith('px'):
|
if size_value.endswith("px"):
|
||||||
try:
|
try:
|
||||||
font_size = int(float(size_value[:-2]))
|
font_size = int(float(size_value[:-2]))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
elif size_value.endswith('pt'):
|
elif size_value.endswith("pt"):
|
||||||
try:
|
try:
|
||||||
font_size = int(float(size_value[:-2]))
|
font_size = int(float(size_value[:-2]))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if 'font-weight' in css_styles:
|
if "font-weight" in css_styles:
|
||||||
weight_value = css_styles['font-weight'].lower()
|
weight_value = css_styles["font-weight"].lower()
|
||||||
if weight_value in ['bold', '700', '800', '900']:
|
if weight_value in ["bold", "700", "800", "900"]:
|
||||||
weight = FontWeight.BOLD
|
weight = FontWeight.BOLD
|
||||||
elif weight_value in ['normal', '400']:
|
elif weight_value in ["normal", "400"]:
|
||||||
weight = FontWeight.NORMAL
|
weight = FontWeight.NORMAL
|
||||||
|
|
||||||
if 'font-style' in css_styles:
|
if "font-style" in css_styles:
|
||||||
style_value = css_styles['font-style'].lower()
|
style_value = css_styles["font-style"].lower()
|
||||||
if style_value == 'italic':
|
if style_value == "italic":
|
||||||
style = FontStyle.ITALIC
|
style = FontStyle.ITALIC
|
||||||
elif style_value == 'normal':
|
elif style_value == "normal":
|
||||||
style = FontStyle.NORMAL
|
style = FontStyle.NORMAL
|
||||||
|
|
||||||
if 'text-decoration' in css_styles:
|
if "text-decoration" in css_styles:
|
||||||
decoration_value = css_styles['text-decoration'].lower()
|
decoration_value = css_styles["text-decoration"].lower()
|
||||||
if 'underline' in decoration_value:
|
if "underline" in decoration_value:
|
||||||
decoration = TextDecoration.UNDERLINE
|
decoration = TextDecoration.UNDERLINE
|
||||||
elif 'line-through' in decoration_value:
|
elif "line-through" in decoration_value:
|
||||||
decoration = TextDecoration.STRIKETHROUGH
|
decoration = TextDecoration.STRIKETHROUGH
|
||||||
elif 'none' in decoration_value:
|
elif "none" in decoration_value:
|
||||||
decoration = TextDecoration.NONE
|
decoration = TextDecoration.NONE
|
||||||
|
|
||||||
if 'color' in css_styles:
|
if "color" in css_styles:
|
||||||
# Parse color (simplified - could be enhanced for hex, rgb, etc.)
|
# Parse color (simplified - could be enhanced for hex, rgb, etc.)
|
||||||
color_value = css_styles['color'].lower()
|
color_value = css_styles["color"].lower()
|
||||||
color_map = {
|
color_map = {
|
||||||
'black': (0, 0, 0),
|
"black": (0, 0, 0),
|
||||||
'white': (255, 255, 255),
|
"white": (255, 255, 255),
|
||||||
'red': (255, 0, 0),
|
"red": (255, 0, 0),
|
||||||
'green': (0, 255, 0),
|
"green": (0, 255, 0),
|
||||||
'blue': (0, 0, 255),
|
"blue": (0, 0, 255),
|
||||||
}
|
}
|
||||||
if color_value in color_map:
|
if color_value in color_map:
|
||||||
colour = color_map[color_value]
|
colour = color_map[color_value]
|
||||||
elif color_value.startswith('#') and len(color_value) == 7:
|
elif color_value.startswith("#") and len(color_value) == 7:
|
||||||
try:
|
try:
|
||||||
r = int(color_value[1:3], 16)
|
r = int(color_value[1:3], 16)
|
||||||
g = int(color_value[3:5], 16)
|
g = int(color_value[3:5], 16)
|
||||||
@ -252,12 +272,13 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s
|
|||||||
style=style,
|
style=style,
|
||||||
decoration=decoration,
|
decoration=decoration,
|
||||||
background=background,
|
background=background,
|
||||||
language=language
|
language=language,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def apply_background_styles(current_background: Optional[Tuple[int, int, int, int]],
|
def apply_background_styles(
|
||||||
css_styles: Dict[str, str]) -> Optional[Tuple[int, int, int, int]]:
|
current_background: Optional[Tuple[int, int, int, int]], css_styles: Dict[str, str]
|
||||||
|
) -> Optional[Tuple[int, int, int, int]]:
|
||||||
"""
|
"""
|
||||||
Apply background styling from CSS.
|
Apply background styling from CSS.
|
||||||
|
|
||||||
@ -268,9 +289,9 @@ def apply_background_styles(current_background: Optional[Tuple[int, int, int, in
|
|||||||
Returns:
|
Returns:
|
||||||
New background color or None
|
New background color or None
|
||||||
"""
|
"""
|
||||||
if 'background-color' in css_styles:
|
if "background-color" in css_styles:
|
||||||
bg_value = css_styles['background-color'].lower()
|
bg_value = css_styles["background-color"].lower()
|
||||||
if bg_value == 'transparent':
|
if bg_value == "transparent":
|
||||||
return None
|
return None
|
||||||
# Add color parsing logic here if needed
|
# Add color parsing logic here if needed
|
||||||
|
|
||||||
@ -301,7 +322,27 @@ def extract_text_content(element: Tag, context: StyleContext) -> List[Word]:
|
|||||||
words.append(Word(word_text, context.font, context.background))
|
words.append(Word(word_text, context.font, context.background))
|
||||||
elif isinstance(child, Tag):
|
elif isinstance(child, Tag):
|
||||||
# Process inline elements
|
# 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_context = apply_element_styling(context, child)
|
||||||
child_words = extract_text_content(child, child_context)
|
child_words = extract_text_content(child, child_context)
|
||||||
words.extend(child_words)
|
words.extend(child_words)
|
||||||
@ -321,7 +362,9 @@ def extract_text_content(element: Tag, context: StyleContext) -> List[Word]:
|
|||||||
return words
|
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.
|
Process a single HTML element using appropriate handler.
|
||||||
|
|
||||||
@ -340,6 +383,7 @@ def process_element(element: Tag, context: StyleContext) -> Union[Block, List[Bl
|
|||||||
# Handler function signatures:
|
# Handler function signatures:
|
||||||
# All handlers receive (element: Tag, context: StyleContext) -> Union[Block, List[Block], None]
|
# All handlers receive (element: Tag, context: StyleContext) -> Union[Block, List[Block], None]
|
||||||
|
|
||||||
|
|
||||||
def paragraph_handler(element: Tag, context: StyleContext) -> Paragraph:
|
def paragraph_handler(element: Tag, context: StyleContext) -> Paragraph:
|
||||||
"""Handle <p> elements."""
|
"""Handle <p> elements."""
|
||||||
paragraph = Paragraph(context.font)
|
paragraph = Paragraph(context.font)
|
||||||
@ -367,12 +411,12 @@ def div_handler(element: Tag, context: StyleContext) -> List[Block]:
|
|||||||
def heading_handler(element: Tag, context: StyleContext) -> Heading:
|
def heading_handler(element: Tag, context: StyleContext) -> Heading:
|
||||||
"""Handle <h1>-<h6> elements."""
|
"""Handle <h1>-<h6> elements."""
|
||||||
level_map = {
|
level_map = {
|
||||||
'h1': HeadingLevel.H1,
|
"h1": HeadingLevel.H1,
|
||||||
'h2': HeadingLevel.H2,
|
"h2": HeadingLevel.H2,
|
||||||
'h3': HeadingLevel.H3,
|
"h3": HeadingLevel.H3,
|
||||||
'h4': HeadingLevel.H4,
|
"h4": HeadingLevel.H4,
|
||||||
'h5': HeadingLevel.H5,
|
"h5": HeadingLevel.H5,
|
||||||
'h6': HeadingLevel.H6,
|
"h6": HeadingLevel.H6,
|
||||||
}
|
}
|
||||||
|
|
||||||
level = level_map.get(element.name.lower(), HeadingLevel.H1)
|
level = level_map.get(element.name.lower(), HeadingLevel.H1)
|
||||||
@ -401,12 +445,12 @@ def blockquote_handler(element: Tag, context: StyleContext) -> Quote:
|
|||||||
|
|
||||||
def preformatted_handler(element: Tag, context: StyleContext) -> CodeBlock:
|
def preformatted_handler(element: Tag, context: StyleContext) -> CodeBlock:
|
||||||
"""Handle <pre> elements."""
|
"""Handle <pre> elements."""
|
||||||
language = context.element_attributes.get('data-language', '')
|
language = context.element_attributes.get("data-language", "")
|
||||||
code_block = CodeBlock(language)
|
code_block = CodeBlock(language)
|
||||||
|
|
||||||
# Preserve whitespace and line breaks in preformatted text
|
# Preserve whitespace and line breaks in preformatted text
|
||||||
text = element.get_text(separator='\n', strip=False)
|
text = element.get_text(separator="\n", strip=False)
|
||||||
for line in text.split('\n'):
|
for line in text.split("\n"):
|
||||||
code_block.add_line(line)
|
code_block.add_line(line)
|
||||||
|
|
||||||
return code_block
|
return code_block
|
||||||
@ -415,7 +459,7 @@ def preformatted_handler(element: Tag, context: StyleContext) -> CodeBlock:
|
|||||||
def code_handler(element: Tag, context: StyleContext) -> Union[CodeBlock, None]:
|
def code_handler(element: Tag, context: StyleContext) -> Union[CodeBlock, None]:
|
||||||
"""Handle <code> elements."""
|
"""Handle <code> elements."""
|
||||||
# If parent is <pre>, this is handled by preformatted_handler
|
# 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
|
return None # Will be handled by parent
|
||||||
|
|
||||||
# Inline code - handled during text extraction
|
# Inline code - handled during text extraction
|
||||||
@ -426,7 +470,7 @@ def unordered_list_handler(element: Tag, context: StyleContext) -> HList:
|
|||||||
"""Handle <ul> elements."""
|
"""Handle <ul> elements."""
|
||||||
hlist = HList(ListStyle.UNORDERED, context.font)
|
hlist = HList(ListStyle.UNORDERED, context.font)
|
||||||
for child in element.children:
|
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)
|
child_context = apply_element_styling(context, child)
|
||||||
item = process_element(child, child_context)
|
item = process_element(child, child_context)
|
||||||
if item:
|
if item:
|
||||||
@ -438,7 +482,7 @@ def ordered_list_handler(element: Tag, context: StyleContext) -> HList:
|
|||||||
"""Handle <ol> elements."""
|
"""Handle <ol> elements."""
|
||||||
hlist = HList(ListStyle.ORDERED, context.font)
|
hlist = HList(ListStyle.ORDERED, context.font)
|
||||||
for child in element.children:
|
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)
|
child_context = apply_element_styling(context, child)
|
||||||
item = process_element(child, child_context)
|
item = process_element(child, child_context)
|
||||||
if item:
|
if item:
|
||||||
@ -477,7 +521,7 @@ def list_item_handler(element: Tag, context: StyleContext) -> ListItem:
|
|||||||
def table_handler(element: Tag, context: StyleContext) -> Table:
|
def table_handler(element: Tag, context: StyleContext) -> Table:
|
||||||
"""Handle <table> elements."""
|
"""Handle <table> elements."""
|
||||||
caption = None
|
caption = None
|
||||||
caption_elem = element.find('caption')
|
caption_elem = element.find("caption")
|
||||||
if caption_elem:
|
if caption_elem:
|
||||||
caption = caption_elem.get_text(strip=True)
|
caption = caption_elem.get_text(strip=True)
|
||||||
|
|
||||||
@ -486,16 +530,16 @@ def table_handler(element: Tag, context: StyleContext) -> Table:
|
|||||||
# Process table rows
|
# Process table rows
|
||||||
for child in element.children:
|
for child in element.children:
|
||||||
if isinstance(child, Tag):
|
if isinstance(child, Tag):
|
||||||
if child.name.lower() == 'tr':
|
if child.name.lower() == "tr":
|
||||||
child_context = apply_element_styling(context, child)
|
child_context = apply_element_styling(context, child)
|
||||||
row = process_element(child, child_context)
|
row = process_element(child, child_context)
|
||||||
if row:
|
if row:
|
||||||
table.add_row(row)
|
table.add_row(row)
|
||||||
elif child.name.lower() in ['thead', 'tbody', 'tfoot']:
|
elif child.name.lower() in ["thead", "tbody", "tfoot"]:
|
||||||
section = 'header' if child.name.lower() == 'thead' else 'body'
|
section = "header" if child.name.lower() == "thead" else "body"
|
||||||
section = 'footer' if child.name.lower() == 'tfoot' else section
|
section = "footer" if child.name.lower() == "tfoot" else section
|
||||||
|
|
||||||
for row_elem in child.find_all('tr'):
|
for row_elem in child.find_all("tr"):
|
||||||
child_context = apply_element_styling(context, row_elem)
|
child_context = apply_element_styling(context, row_elem)
|
||||||
row = process_element(row_elem, child_context)
|
row = process_element(row_elem, child_context)
|
||||||
if row:
|
if row:
|
||||||
@ -508,7 +552,7 @@ def table_row_handler(element: Tag, context: StyleContext) -> TableRow:
|
|||||||
"""Handle <tr> elements."""
|
"""Handle <tr> elements."""
|
||||||
row = TableRow(context.font)
|
row = TableRow(context.font)
|
||||||
for child in element.children:
|
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)
|
child_context = apply_element_styling(context, child)
|
||||||
cell = process_element(child, child_context)
|
cell = process_element(child, child_context)
|
||||||
if cell:
|
if cell:
|
||||||
@ -518,8 +562,8 @@ def table_row_handler(element: Tag, context: StyleContext) -> TableRow:
|
|||||||
|
|
||||||
def table_cell_handler(element: Tag, context: StyleContext) -> TableCell:
|
def table_cell_handler(element: Tag, context: StyleContext) -> TableCell:
|
||||||
"""Handle <td> elements."""
|
"""Handle <td> elements."""
|
||||||
colspan = int(context.element_attributes.get('colspan', 1))
|
colspan = int(context.element_attributes.get("colspan", 1))
|
||||||
rowspan = int(context.element_attributes.get('rowspan', 1))
|
rowspan = int(context.element_attributes.get("rowspan", 1))
|
||||||
cell = TableCell(False, colspan, rowspan, context.font)
|
cell = TableCell(False, colspan, rowspan, context.font)
|
||||||
|
|
||||||
# Process cell content
|
# Process cell content
|
||||||
@ -549,8 +593,8 @@ def table_cell_handler(element: Tag, context: StyleContext) -> TableCell:
|
|||||||
|
|
||||||
def table_header_cell_handler(element: Tag, context: StyleContext) -> TableCell:
|
def table_header_cell_handler(element: Tag, context: StyleContext) -> TableCell:
|
||||||
"""Handle <th> elements."""
|
"""Handle <th> elements."""
|
||||||
colspan = int(context.element_attributes.get('colspan', 1))
|
colspan = int(context.element_attributes.get("colspan", 1))
|
||||||
rowspan = int(context.element_attributes.get('rowspan', 1))
|
rowspan = int(context.element_attributes.get("rowspan", 1))
|
||||||
cell = TableCell(True, colspan, rowspan, context.font)
|
cell = TableCell(True, colspan, rowspan, context.font)
|
||||||
|
|
||||||
# Process cell content (same as td)
|
# Process cell content (same as td)
|
||||||
@ -590,16 +634,16 @@ def line_break_handler(element: Tag, context: StyleContext) -> None:
|
|||||||
|
|
||||||
def image_handler(element: Tag, context: StyleContext) -> Image:
|
def image_handler(element: Tag, context: StyleContext) -> Image:
|
||||||
"""Handle <img> elements."""
|
"""Handle <img> elements."""
|
||||||
src = context.element_attributes.get('src', '')
|
src = context.element_attributes.get("src", "")
|
||||||
alt_text = context.element_attributes.get('alt', '')
|
alt_text = context.element_attributes.get("alt", "")
|
||||||
|
|
||||||
# Parse dimensions if provided
|
# Parse dimensions if provided
|
||||||
width = height = None
|
width = height = None
|
||||||
try:
|
try:
|
||||||
if 'width' in context.element_attributes:
|
if "width" in context.element_attributes:
|
||||||
width = int(context.element_attributes['width'])
|
width = int(context.element_attributes["width"])
|
||||||
if 'height' in context.element_attributes:
|
if "height" in context.element_attributes:
|
||||||
height = int(context.element_attributes['height'])
|
height = int(context.element_attributes["height"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -619,72 +663,70 @@ def generic_handler(element: Tag, context: StyleContext) -> List[Block]:
|
|||||||
# Handler registry - maps HTML tag names to handler functions
|
# Handler registry - maps HTML tag names to handler functions
|
||||||
HANDLERS: Dict[str, Callable[[Tag, StyleContext], Union[Block, List[Block], None]]] = {
|
HANDLERS: Dict[str, Callable[[Tag, StyleContext], Union[Block, List[Block], None]]] = {
|
||||||
# Block elements
|
# Block elements
|
||||||
'p': paragraph_handler,
|
"p": paragraph_handler,
|
||||||
'div': div_handler,
|
"div": div_handler,
|
||||||
'h1': heading_handler,
|
"h1": heading_handler,
|
||||||
'h2': heading_handler,
|
"h2": heading_handler,
|
||||||
'h3': heading_handler,
|
"h3": heading_handler,
|
||||||
'h4': heading_handler,
|
"h4": heading_handler,
|
||||||
'h5': heading_handler,
|
"h5": heading_handler,
|
||||||
'h6': heading_handler,
|
"h6": heading_handler,
|
||||||
'blockquote': blockquote_handler,
|
"blockquote": blockquote_handler,
|
||||||
'pre': preformatted_handler,
|
"pre": preformatted_handler,
|
||||||
'code': code_handler,
|
"code": code_handler,
|
||||||
'ul': unordered_list_handler,
|
"ul": unordered_list_handler,
|
||||||
'ol': ordered_list_handler,
|
"ol": ordered_list_handler,
|
||||||
'li': list_item_handler,
|
"li": list_item_handler,
|
||||||
'table': table_handler,
|
"table": table_handler,
|
||||||
'tr': table_row_handler,
|
"tr": table_row_handler,
|
||||||
'td': table_cell_handler,
|
"td": table_cell_handler,
|
||||||
'th': table_header_cell_handler,
|
"th": table_header_cell_handler,
|
||||||
'hr': horizontal_rule_handler,
|
"hr": horizontal_rule_handler,
|
||||||
'br': line_break_handler,
|
"br": line_break_handler,
|
||||||
|
|
||||||
# Semantic elements (treated as containers)
|
# Semantic elements (treated as containers)
|
||||||
'section': div_handler,
|
"section": div_handler,
|
||||||
'article': div_handler,
|
"article": div_handler,
|
||||||
'aside': div_handler,
|
"aside": div_handler,
|
||||||
'nav': div_handler,
|
"nav": div_handler,
|
||||||
'header': div_handler,
|
"header": div_handler,
|
||||||
'footer': div_handler,
|
"footer": div_handler,
|
||||||
'main': div_handler,
|
"main": div_handler,
|
||||||
'figure': div_handler,
|
"figure": div_handler,
|
||||||
'figcaption': paragraph_handler,
|
"figcaption": paragraph_handler,
|
||||||
|
|
||||||
# Media elements
|
# Media elements
|
||||||
'img': image_handler,
|
"img": image_handler,
|
||||||
|
|
||||||
# Inline elements (handled during text extraction)
|
# Inline elements (handled during text extraction)
|
||||||
'span': ignore_handler,
|
"span": ignore_handler,
|
||||||
'a': ignore_handler,
|
"a": ignore_handler,
|
||||||
'strong': ignore_handler,
|
"strong": ignore_handler,
|
||||||
'b': ignore_handler,
|
"b": ignore_handler,
|
||||||
'em': ignore_handler,
|
"em": ignore_handler,
|
||||||
'i': ignore_handler,
|
"i": ignore_handler,
|
||||||
'u': ignore_handler,
|
"u": ignore_handler,
|
||||||
's': ignore_handler,
|
"s": ignore_handler,
|
||||||
'del': ignore_handler,
|
"del": ignore_handler,
|
||||||
'ins': ignore_handler,
|
"ins": ignore_handler,
|
||||||
'mark': ignore_handler,
|
"mark": ignore_handler,
|
||||||
'small': ignore_handler,
|
"small": ignore_handler,
|
||||||
'sub': ignore_handler,
|
"sub": ignore_handler,
|
||||||
'sup': ignore_handler,
|
"sup": ignore_handler,
|
||||||
'q': ignore_handler,
|
"q": ignore_handler,
|
||||||
'cite': ignore_handler,
|
"cite": ignore_handler,
|
||||||
'abbr': ignore_handler,
|
"abbr": ignore_handler,
|
||||||
'time': ignore_handler,
|
"time": ignore_handler,
|
||||||
|
|
||||||
# Ignored elements
|
# Ignored elements
|
||||||
'script': ignore_handler,
|
"script": ignore_handler,
|
||||||
'style': ignore_handler,
|
"style": ignore_handler,
|
||||||
'meta': ignore_handler,
|
"meta": ignore_handler,
|
||||||
'link': ignore_handler,
|
"link": ignore_handler,
|
||||||
'head': ignore_handler,
|
"head": ignore_handler,
|
||||||
'title': 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.
|
Parse HTML string and return list of Block objects.
|
||||||
|
|
||||||
@ -695,12 +737,12 @@ def parse_html_string(html_string: str, base_font: Optional[Font] = None) -> Lis
|
|||||||
Returns:
|
Returns:
|
||||||
List of Block objects representing the document structure
|
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)
|
context = create_base_context(base_font)
|
||||||
blocks = []
|
blocks = []
|
||||||
|
|
||||||
# Process the body if it exists, otherwise process all top-level elements
|
# 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:
|
for element in root_element.children:
|
||||||
if isinstance(element, Tag):
|
if isinstance(element, Tag):
|
||||||
@ -713,27 +755,3 @@ def parse_html_string(html_string: str, base_font: Optional[Font] = None) -> Lis
|
|||||||
blocks.append(result)
|
blocks.append(result)
|
||||||
|
|
||||||
return blocks
|
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",
|
"pyphen",
|
||||||
"beautifulsoup4",
|
"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
|
# Run tests with coverage
|
||||||
print("\n2. Running 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")
|
run_command(test_cmd, "Running tests with coverage")
|
||||||
|
|
||||||
# Generate test coverage badge
|
# Generate test coverage badge
|
||||||
@ -89,7 +89,7 @@ else:
|
|||||||
|
|
||||||
# List generated files
|
# List generated files
|
||||||
print("\n6. 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:
|
for file in files:
|
||||||
if os.path.exists(file):
|
if os.path.exists(file):
|
||||||
print(f" ✓ {file}")
|
print(f" ✓ {file}")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user