Files
omniread/omniread/html/parser.py
Vishesh 'ironeagle' Bangotra 650d3a19e8 chore(release): 0.0.3
Squash-merge lib-standardisation: adopt repo standards across packaging,
tooling, docs, and CI.

- standardize pyproject.toml (canonical packaging, lint config, extras, GSDFC pydoclint)
- add py.typed marker, modernize type hints and Google-style docstrings
- nest generated docs under docs/lib and docs/mcp; add hand-written wiki
- adopt standard Type A PyPI CI pipeline (tag==version guard + quality gate)
- promote generated CHANGELOG.md; bump version to 0.0.3
2026-10-17 02:16:48 +05:30

203 lines
5.6 KiB
Python

"""
# Summary
HTML parser base implementations for OmniRead.
This module provides reusable HTML parsing utilities built on top of
the abstract parser contracts defined in `omniread.core.parser`.
It supplies:
- Content-type enforcement for HTML inputs
- BeautifulSoup initialization and lifecycle management
- Common helper methods for extracting structured data from HTML elements
Concrete parsers must subclass `HTMLParser` and implement the `parse()` method
to return a structured representation appropriate for their use case.
"""
from abc import abstractmethod
from typing import Any, Generic, TypeVar
from bs4 import BeautifulSoup, Tag
from omniread.core.content import Content, ContentType
from omniread.core.parser import BaseParser
T = TypeVar("T")
class HTMLParser(BaseParser[T], Generic[T]):
"""
Base HTML parser.
Notes:
**Responsibilities:**
- This class extends the core `BaseParser` with HTML-specific behavior,
including DOM parsing via BeautifulSoup and reusable extraction helpers.
- Provides reusable helpers for HTML extraction. Concrete parsers must
explicitly define the return type.
**Guarantees:**
- Accepts only HTML content.
- Owns a parsed BeautifulSoup DOM tree.
- Provides pure helper utilities for common HTML structures.
**Constraints:**
- Concrete subclasses must define the output type `T` and implement
the `parse()` method.
"""
supported_types = {ContentType.HTML}
"""
Set of content types supported by this parser (HTML only).
"""
def __init__(self, content: Content, features: str = "html.parser"):
"""
Initialize the HTML parser.
Args:
content (Content):
HTML content to be parsed.
features (str, optional):
BeautifulSoup parser backend to use (e.g., 'html.parser', 'lxml').
Raises:
ValueError:
If the content is empty or not valid HTML.
"""
super().__init__(content)
self._features = features
self._soup = self._get_soup()
# ----------------------------
# Contract
# ----------------------------
@abstractmethod
def parse(self) -> T:
"""
Fully parse the HTML content into structured output.
Returns:
T:
Parsed representation of type `T`.
Notes:
**Responsibilities:**
- Implementations must fully interpret the HTML DOM and return a
deterministic, structured output.
"""
raise NotImplementedError
# ----------------------------
# Helpers (static / pure)
# ----------------------------
@staticmethod
def parse_div(div: Tag, *, separator: str = " ") -> str:
"""
Extract normalized text from a `<div>` element.
Args:
div (Tag):
BeautifulSoup tag representing a `<div>`.
separator (str, optional):
String used to separate text nodes.
Returns:
str:
Flattened, whitespace-normalized text content.
"""
return div.get_text(separator=separator, strip=True)
@staticmethod
def parse_link(a: Tag) -> str | None:
"""
Extract the hyperlink reference from an `<a>` element.
Args:
a (Tag):
BeautifulSoup tag representing an anchor.
Returns:
str | None:
The value of the `href` attribute, or None if absent.
"""
return a.get("href")
@staticmethod
def parse_table(table: Tag) -> list[list[str]]:
"""
Parse an HTML table into a 2D list of strings.
Args:
table (Tag):
BeautifulSoup tag representing a `<table>`.
Returns:
list[list[str]]:
A list of rows, where each row is a list of cell text values.
"""
rows: list[list[str]] = []
for tr in table.find_all("tr"):
cells = [cell.get_text(strip=True) for cell in tr.find_all(["td", "th"])]
if cells:
rows.append(cells)
return rows
# ----------------------------
# Helpers (instance-level)
# ----------------------------
def _get_soup(self) -> BeautifulSoup:
"""
Build a BeautifulSoup DOM tree from raw HTML content.
Returns:
BeautifulSoup:
Parsed BeautifulSoup document tree.
Raises:
ValueError:
If the content payload is empty.
"""
if not self.content.raw:
raise ValueError("Empty HTML content")
return BeautifulSoup(self.content.raw, features=self._features)
def parse_meta(self) -> dict[str, Any]:
"""
Extract high-level metadata from the HTML document.
Returns:
dict[str, Any]:
Dictionary containing extracted metadata.
Notes:
**Responsibilities:**
- Extract high-level metadata from the HTML document.
- This includes: Document title, `<meta>` tag name/property to
content mappings.
"""
soup = self._soup
title = soup.title.string.strip() if soup.title and soup.title.string else None
meta = {
tag.get("name") or tag.get("property"): tag.get("content")
for tag in soup.find_all("meta")
if tag.get("content")
}
return {
"title": title,
"meta": meta,
}