Coverage for rdgai/tei.py: 100.00%
113 statements
« prev ^ index » next coverage.py v7.11.1, created at 2026-07-03 02:54 +0000
« prev ^ index » next coverage.py v7.11.1, created at 2026-07-03 02:54 +0000
1import re
2from pathlib import Path
3from lxml import etree as ET
4from lxml.etree import _ElementTree as ElementTree
5from lxml.etree import _Element as Element
7from .languages import convert_language_code
10def get_language_code(doc:ElementTree|Element) -> str:
11 """ Reads the element <text> and returns the value of the xml:lang attribute."""
12 text = find_element(doc, ".//text")
13 if text is None:
14 return ""
16 return text.attrib.get("{http://www.w3.org/XML/1998/namespace}lang", "")
19def get_language(doc:ElementTree|Element) -> str:
20 code = get_language_code(doc)
21 return convert_language_code(code)
24def make_nc_name(string):
25 invalid_chars = "!\"#$%&'()*+/:;<=>?@[\\]^,{|}~` "
26 result = string.translate(str.maketrans(invalid_chars, '_' * len(invalid_chars)))
27 # if result[0].isdigit or result[0] in [".", "-"]:
28 # result = "id-" + result
30 return result
33def extract_text(node:Element, include_tail:bool=True, sep:str=" ") -> str:
34 if node is None:
35 return ""
37 if isinstance(node.tag, str):
38 tag = re.sub(r"{.*}", "", node.tag)
39 else:
40 return ""
42 if tag in ["pc", "witDetail", "note"]:
43 return ""
44 if tag == "app":
45 lemma = find_element(node, ".//lem")
46 if lemma is None:
47 lemma = find_element(node, ".//rdg")
48 return extract_text(lemma) or ""
49 if tag == "ref":
50 root = node.getroottree().getroot()
51 target_id = node.attrib['target'].lstrip("#")
52 ns = {"tei": "http://www.tei-c.org/ns/1.0"}
53 target = root.xpath(f"//*[@xml:id='{target_id}']", namespaces=ns)
55 if target:
56 return extract_text(target[0])
58 if tag == "w":
59 sep = ""
61 text = node.text or ""
62 for child in node:
63 text += sep + extract_text(child, sep=sep)
65 if include_tail and node.tail:
66 text += sep + node.tail
68 return text.strip()
71def read_tei(path:Path) -> ElementTree:
72 parser = ET.XMLParser(remove_blank_text=True)
73 with open(path, 'r') as f:
74 return ET.parse(f, parser)
77def find_element(doc:ElementTree|Element, xpath:str) -> Element|None:
78 assert doc is not None, f"Document is None in find_element({doc}, {xpath})"
79 if isinstance(doc, ElementTree):
80 doc = doc.getroot()
81 namespaces = doc.nsmap | {"xml": "http://www.w3.org/XML/1998/namespace"}
82 element = doc.find(xpath, namespaces=namespaces)
83 if element is None:
84 try:
85 element = doc.find(xpath)
86 except SyntaxError:
87 return None
88 return element
91def find_elements(doc:ElementTree|Element|None, xpath:str) -> list[Element]:
92 if doc is None:
93 return []
94 if isinstance(doc, ElementTree):
95 doc = doc.getroot()
96 namespaces = doc.nsmap | {"xml": "http://www.w3.org/XML/1998/namespace"}
97 results = doc.findall(xpath, namespaces=namespaces)
98 results += doc.findall(xpath)
99 return results
102def find_parent(element:Element, tag:str) -> Element|None:
103 """
104 Finds the nearest ancestor of the given element with the specified tag.
106 Args:
107 element (Element): The starting XML element from which to search upward.
108 tag (str): The tag name of the ancestor element to find.
110 Returns:
111 Optional[Element]: The nearest ancestor element with the specified tag, or None if no such element is found.
113 Example:
114 >>> from xml.etree.ElementTree import Element
115 >>> root = Element('root')
116 >>> ab = Element('ab')
117 >>> section = Element('section')
118 >>> target = Element('target')
119 >>> root.append(ab)
120 >>> ab.append(section)
121 >>> section.append(target)
122 >>> result = find_parent(target, 'ab')
123 >>> assert result == ab
125 This will find the <ab> ancestor of the <target> element.
126 """
127 while element is not None:
128 element_tag = re.sub(r"{.*}", "", element.tag)
129 if element_tag == tag:
130 return element
131 element = element.getparent()
132 return None
135def write_tei(doc:ElementTree, path:Path|str) -> None:
136 Path(path).parent.mkdir(parents=True, exist_ok=True)
137 doc.write(str(path), encoding="utf-8", xml_declaration=True, pretty_print=True)
140def get_reading_identifier(reading:Element, check:bool=False, create_if_necessary:bool=True) -> str:
141 identifier = reading.attrib.get("{http://www.w3.org/XML/1998/namespace}id", "")
142 if not identifier:
143 identifier = reading.attrib.get("n", "")
145 if not identifier and create_if_necessary:
146 app = reading.getparent()
147 identifier = 1
148 while find_element(app, f".//rdg[@n='{identifier}']") is not None:
149 identifier += 1
150 identifier = str(identifier)
151 reading.attrib["n"] = identifier
153 if check:
154 assert identifier, f"Reading {reading} must have a name attribute 'xml:id' or 'n'."
156 return identifier
159def extract_text_siblings(element:Element, until_tag:str, preceding:bool=False, truncate:int|None=None) -> str:
160 """
161 Extracts text from sibling elements of the given element until a sibling with the specified tag is encountered.
163 Args:
164 element (Element): The starting XML element from which to extract text from its siblings.
165 until_tag (str): The tag name of the sibling element that will stop the extraction process.
166 preceding (bool): If True, extracts text from preceding siblings; if False, extracts from following siblings.
167 truncate (int|None): If specified, limits the number of words extracted to this value.
169 Returns:
170 str: A string containing the concatenated text from the sibling elements, up to the specified limit if provided.
171 """
172 words = []
173 for sibling in element.itersiblings(preceding=preceding):
174 tag = sibling.tag
175 if isinstance(tag, str) and tag.rsplit("}", 1)[-1] == until_tag:
176 break
178 text = extract_text(sibling)
179 if text:
180 my_words = text.split()
181 if preceding:
182 # Reverse the order of words for preceding context because we are iterating backwards through the siblings.
183 my_words.reverse()
185 words.extend(my_words)
186 if truncate and len(words) >= truncate:
187 words = words[:truncate]
188 break
190 if preceding:
191 words.reverse()
193 return " ".join(words)