phml.helpers

  1import sys
  2from pathlib import Path
  3from traceback import print_tb
  4from typing import Any, Iterator
  5
  6from phml.nodes import AST, Element, Node, Parent
  7
  8
  9def build_recursive_context(node: Node, context: dict[str, Any]) -> dict[str, Any]:
 10    """Build recursive context for the current node."""
 11    parent = node.parent
 12    parents = []
 13    result = {**context}
 14
 15    while parent is not None and not isinstance(parent, AST):
 16        parents.append(parent)
 17        parent = parent.parent
 18
 19    for parent in parents:
 20        result.update(parent.context)
 21
 22    if isinstance(node, Element):
 23        result.update(node.context)
 24    return result
 25
 26
 27def iterate_nodes(node: Parent) -> Iterator[Node]:
 28    """Recursively iterate over nodes and their children."""
 29    yield node
 30    for child in node:
 31        if isinstance(child, Parent):
 32            yield from iterate_nodes(child)
 33
 34
 35def calc_offset(content: str) -> int:
 36    """Get the leading offset of the first line of the string."""
 37    return len(content) - len(content.lstrip())
 38
 39
 40def strip_blank_lines(data: str) -> str:
 41    """Strip the blank lines at the start and end of a list."""
 42    data = data.rstrip().replace("\r\n", "\n")
 43    data_lines = data.split("\n")
 44
 45    # remove leading blank lines
 46    for idx in range(0, len(data_lines)):  # pylint: disable=consider-using-enumerate
 47        if data_lines[idx].strip() != "":
 48            return "\n".join(data_lines[idx:])
 49
 50    return ""
 51
 52
 53def normalize_indent(content: str, indent: int = 0) -> str:
 54    """Normalize the indent between all lines.
 55
 56    Args:
 57        content (str): The content to normalize the indent for
 58        indent (bool): The amount of offset to add to each line after normalization.
 59
 60    Returns:
 61        str: The normalized string
 62    """
 63
 64    lines = strip_blank_lines(content).split("\n")
 65    offset = calc_offset(lines[0])
 66
 67    result = []
 68    for line in lines:
 69        if len(line) > 0:
 70            result.append(
 71                " " * indent + line[min(calc_offset(line), offset) :],
 72            )
 73        else:
 74            result.append(line)
 75    return "\n".join(result)
 76
 77
 78class PHMLTryCatch:
 79    """Context manager around core PHML actions. When an exception is raised
 80    it is caught here and the current file that is being handled is prepended
 81    to the exception message.
 82    """
 83
 84    def __init__(self, path: str | Path | None = None, fallback: str = "") -> None:
 85        if path is None or str(path) == "":
 86            path = fallback
 87        self._path = str(path or fallback)
 88
 89    def __enter__(self):
 90        pass
 91
 92    #           (self, exc_type, exc_val, exc_tb)
 93    def __exit__(self, _, exc_val, exc_tb):
 94        if exc_val is not None and not isinstance(exc_val, SystemExit):
 95            print_tb(exc_tb)
 96            if self._path != "":
 97                sys.stderr.write(f"[{self._path}]: {exc_val}")
 98            else:
 99                sys.stderr.write(str(exc_val))
100            sys.stderr.flush()
101            exit()
def build_recursive_context( node: phml.nodes.Node, context: dict[str, typing.Any]) -> dict[str, typing.Any]:
10def build_recursive_context(node: Node, context: dict[str, Any]) -> dict[str, Any]:
11    """Build recursive context for the current node."""
12    parent = node.parent
13    parents = []
14    result = {**context}
15
16    while parent is not None and not isinstance(parent, AST):
17        parents.append(parent)
18        parent = parent.parent
19
20    for parent in parents:
21        result.update(parent.context)
22
23    if isinstance(node, Element):
24        result.update(node.context)
25    return result

Build recursive context for the current node.

def iterate_nodes(node: phml.nodes.Parent) -> Iterator[phml.nodes.Node]:
28def iterate_nodes(node: Parent) -> Iterator[Node]:
29    """Recursively iterate over nodes and their children."""
30    yield node
31    for child in node:
32        if isinstance(child, Parent):
33            yield from iterate_nodes(child)

Recursively iterate over nodes and their children.

def calc_offset(content: str) -> int:
36def calc_offset(content: str) -> int:
37    """Get the leading offset of the first line of the string."""
38    return len(content) - len(content.lstrip())

Get the leading offset of the first line of the string.

def strip_blank_lines(data: str) -> str:
41def strip_blank_lines(data: str) -> str:
42    """Strip the blank lines at the start and end of a list."""
43    data = data.rstrip().replace("\r\n", "\n")
44    data_lines = data.split("\n")
45
46    # remove leading blank lines
47    for idx in range(0, len(data_lines)):  # pylint: disable=consider-using-enumerate
48        if data_lines[idx].strip() != "":
49            return "\n".join(data_lines[idx:])
50
51    return ""

Strip the blank lines at the start and end of a list.

def normalize_indent(content: str, indent: int = 0) -> str:
54def normalize_indent(content: str, indent: int = 0) -> str:
55    """Normalize the indent between all lines.
56
57    Args:
58        content (str): The content to normalize the indent for
59        indent (bool): The amount of offset to add to each line after normalization.
60
61    Returns:
62        str: The normalized string
63    """
64
65    lines = strip_blank_lines(content).split("\n")
66    offset = calc_offset(lines[0])
67
68    result = []
69    for line in lines:
70        if len(line) > 0:
71            result.append(
72                " " * indent + line[min(calc_offset(line), offset) :],
73            )
74        else:
75            result.append(line)
76    return "\n".join(result)

Normalize the indent between all lines.

Args
  • content (str): The content to normalize the indent for
  • indent (bool): The amount of offset to add to each line after normalization.
Returns

str: The normalized string

class PHMLTryCatch:
 79class PHMLTryCatch:
 80    """Context manager around core PHML actions. When an exception is raised
 81    it is caught here and the current file that is being handled is prepended
 82    to the exception message.
 83    """
 84
 85    def __init__(self, path: str | Path | None = None, fallback: str = "") -> None:
 86        if path is None or str(path) == "":
 87            path = fallback
 88        self._path = str(path or fallback)
 89
 90    def __enter__(self):
 91        pass
 92
 93    #           (self, exc_type, exc_val, exc_tb)
 94    def __exit__(self, _, exc_val, exc_tb):
 95        if exc_val is not None and not isinstance(exc_val, SystemExit):
 96            print_tb(exc_tb)
 97            if self._path != "":
 98                sys.stderr.write(f"[{self._path}]: {exc_val}")
 99            else:
100                sys.stderr.write(str(exc_val))
101            sys.stderr.flush()
102            exit()

Context manager around core PHML actions. When an exception is raised it is caught here and the current file that is being handled is prepended to the exception message.

PHMLTryCatch(path: str | pathlib.Path | None = None, fallback: str = '')
85    def __init__(self, path: str | Path | None = None, fallback: str = "") -> None:
86        if path is None or str(path) == "":
87            path = fallback
88        self._path = str(path or fallback)