1"""xCal parser class to fully parse the content."""
2
3from __future__ import annotations
4
5from collections.abc import Sequence
6from typing import TYPE_CHECKING
7
8from icalendar.error import XCalParsingError
9from icalendar.parser.parameter import Parameters
10from icalendar.parser.xcal.adapter import ChildElementAdapter, ElementAdapter
11
12if TYPE_CHECKING:
13 from collections.abc import Sequence
14 from xml.etree.ElementTree import Element
15
16
17class InvalidParserState(Exception):
18 """The parser is in an invalid state.
19
20 This exception should never be raised in production code.
21 """
22
23
24class XCalParser:
25 """Create a new XCalParser that takes care of the parsing state.
26
27 An element is passed to the parser.
28 The element and its child elements can be inspected and marked as parsed.
29 """
30
31 def __init__(self, element: Element | ElementAdapter) -> None:
32 self._element = ElementAdapter.with_element(element)
33 self._children = self._element.children
34 self._consumed = 0
35 self._element_is_parsed = False
36
37 @property
38 def element(self) -> ElementAdapter:
39 """The element to parse."""
40 return self._element
41
42 def is_finished(self) -> bool:
43 """Whether there are more elements left to parse."""
44 return len(self._children) == 0
45
46 @property
47 def child(self) -> ChildElementAdapter:
48 """Get the next child element.
49
50 Raises:
51 InvalidParserState: If there are no more children.
52 """
53 if self.is_finished():
54 raise InvalidParserState("No more children available.")
55 return self._children[0]
56
57 def done(self):
58 """Done with this child element.
59
60 Raises:
61 InvalidParserState: If there are no more children.
62 """
63 if self.is_finished():
64 raise InvalidParserState("No more children available.")
65 self._children = self._children[1:]
66 self._consumed += 1
67
68 @property
69 def tag(self) -> str:
70 """The tag of the element on which to work."""
71 return self._element.tag
72
73 @staticmethod
74 def _sanitize_tags(tags: str | Sequence[str]) -> set[str]:
75 """Return a set of tags to test."""
76 if isinstance(tags, str):
77 return {tags.lower()}
78 return {t.lower() for t in tags}
79
80 def parse_optional_tag(self, tag: str | Sequence[str]) -> ElementAdapter | None:
81 """Find a child tag and return it if present, else ``None``.
82
83 This child is then considered parsed.
84 """
85 tags = self._sanitize_tags(tag)
86 return self._parse_tag(tags)
87
88 def parse_tag(self, tag: str | Sequence[str] | None = None) -> ElementAdapter:
89 """Find a child tag and return it.
90
91 This child is then considered parsed.
92
93 Returns:
94 The child element with the tag, or if no tag is given, then the first child.
95
96 Raises:
97 XCalParsingError: If the tag is not found.
98 """
99 if tag is None:
100 child = self.child
101 self.done()
102 return child
103 tags = self._sanitize_tags(tag)
104 element = self._parse_tag(tags)
105
106 if element is None:
107 raise XCalParsingError(
108 f"Tag {' or '.join(tags)} not found", None, self._element
109 )
110 return element
111
112 def _parse_tag(self, tags: set[str]) -> ElementAdapter | None:
113 """Find a child tag and return it.
114
115 This child is then considered parsed.
116 """
117 for i, child in enumerate(self._children):
118 if child.tag in tags:
119 del self._children[i]
120 self._consumed += 1
121 return child
122 if not self._element_is_parsed and self._element.tag in tags:
123 self._element_is_parsed = True
124 self._consumed += 1
125 return self._element
126 return None
127
128 def parse_tags(self, tag: str | Sequence[str]) -> list[ElementAdapter]:
129 """Find all child tags and return it.
130
131 These children is then considered parsed.
132 """
133 tags = self._sanitize_tags(tag)
134 children = []
135 while True:
136 child = self._parse_tag(tags)
137 if child is None:
138 break
139 children.append(child)
140 return children
141
142 def ensure_tag_is_present(self, tag: str | Sequence[str]) -> str:
143 """Check for the presence of a tag that has not yet been parsed.
144
145 This serves as a check before :meth:`parse_tag`.
146 The returned tag is not marked as parsed.
147
148 Returns:
149 The tag that is present.
150
151 Raises:
152 XCalParsingError: If the tag is not found.
153 """
154 tags = self._sanitize_tags(tag)
155 if not self._element_is_parsed and self._element.tag in tags:
156 return self._element.tag
157 for child in self._children:
158 if child.tag in tags:
159 return child.tag
160 raise XCalParsingError(
161 f"Tag {' or '.join(tags)} not found", None, self._element
162 )
163
164 def parse_parameters(self) -> Parameters:
165 """Return empty parameters.
166
167 This can be changed in subclasses.
168 """
169 return Parameters()
170
171
172__all__ = ["XCalParser"]