Loading src/saref_pypeline/docgen/ts2md_extractor.py +47 −16 Original line number Original line Diff line number Diff line Loading @@ -409,8 +409,12 @@ class TS2MDExtractor: i += 1 i += 1 code_char = 65 # A code_char = 65 # A annexes_file = Path(self.out_folder, "annexes.md") annexes_file.write_text("", encoding="utf-8") while md := self.extract_clause(rf"^Annex {chr(code_char)} ", True): while md := self.extract_clause(rf"^Annex {chr(code_char)} ", True): Path(self.out_folder, f"annex_{chr(code_char)}.md").write_text(md, "utf-8") with annexes_file.open("a", encoding="utf-8") as f: f.write(md) f.write("\n") code_char += 1 code_char += 1 logger.debug(f"Extraction complete: {self.out_folder}") logger.debug(f"Extraction complete: {self.out_folder}") Loading Loading @@ -516,6 +520,7 @@ class TS2MDExtractor: md_output = [] md_output = [] for block_item in iter_block_items(self.document, text, include_title): for block_item in iter_block_items(self.document, text, include_title): md = self.extract_block_item(block_item) md = self.extract_block_item(block_item) if md: md_output.append(md) md_output.append(md) return "\n".join(md_output) return "\n".join(md_output) Loading Loading @@ -684,9 +689,13 @@ class TS2MDExtractor: left, right = md.split("\t", 1) left, right = md.split("\t", 1) content = right.replace("\n", "\n ") content = right.replace("\n", "\n ") return f'!!! example "{left}"\n{content}\n' return f'!!! example "{left}"\n{content}\n' else: elif md.startswith("["): # Reference # Reference return f"* {md}" return f"* {md}" else: # last item of abbreviations: left, right = md.split("\t", 1) return f"* {left}: {right}" def extract_EW( def extract_EW( self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD Loading Loading @@ -772,6 +781,9 @@ class TS2MDExtractor: # exception if in table # exception if in table if paragraph._element.getparent().tag == qn("w:tc"): if paragraph._element.getparent().tag == qn("w:tc"): return self.extract_TB1(paragraph, extract_format) return self.extract_TB1(paragraph, extract_format) # return nothing if empty paragraph if not paragraph.text: return None return self.extract_B_plus(paragraph, 0, extract_format) return self.extract_B_plus(paragraph, 0, extract_format) def extract_B2_plus( def extract_B2_plus( Loading @@ -797,8 +809,8 @@ class TS2MDExtractor: self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD ): ): """Bulleted (letters) indent 1""" """Bulleted (letters) indent 1""" md = self.extract_inner_content(paragraph, extract_format) html = self.extract_inner_content(paragraph, ExtractFormat.HTML) return f"a. {md}" return f'<p data-docx-pstyle="BL">{html}</p>' # General styles For different items # General styles For different items Loading Loading @@ -846,10 +858,14 @@ class TS2MDExtractor: return "" return "" label, caption = match.group(1), match.group(2) label, caption = match.group(1), match.group(2) filename, width = self.get_paragraph_image_info(paragraph) filename, width, crop = self.get_paragraph_image_info(paragraph) if filename: if filename: return f"""<figure id="{label.replace(" ","_")}"> if crop: <img data-docx-width="{width/Cm(1):.2f}cm" src="diagrams/{os.path.basename(filename)}" alt="{caption}"/> crop_attr = " ".join(f' data-docx-crop-{k}="{v}"' for k,v in crop.items()) else: crop_attr = "" return f"""<figure> <img data-docx-width="{width/Cm(1):.2f}cm"{crop_attr} src="diagrams/{os.path.basename(filename)}" alt="{caption}"/> <figcaption>{label}: {caption}</figcaption> <figcaption>{label}: {caption}</figcaption> </figure>\n""" </figure>\n""" else: else: Loading Loading @@ -930,7 +946,7 @@ class TS2MDExtractor: return (caption_id, caption_text). Otherwise, return ("Table_id_unknown", "label_unknown"). return (caption_id, caption_text). Otherwise, return ("Table_id_unknown", "label_unknown"). """ """ if i > 3: if i > 3: return "Table_id_unknown", "label_unknown" return None, None prev = get_prev_block(self.document, table) prev = get_prev_block(self.document, table) if isinstance(prev, Paragraph) and getattr(prev.style, "name", None) == "TH": if isinstance(prev, Paragraph) and getattr(prev.style, "name", None) == "TH": md = self.extract_inner_content(prev) md = self.extract_inner_content(prev) Loading @@ -949,7 +965,7 @@ class TS2MDExtractor: parts = [] parts = [] for first, last, p in with_flags(cell.paragraphs): for first, last, p in with_flags(cell.paragraphs): html = self.extract_paragraph(p, ExtractFormat.HTML) html = self.extract_paragraph(p, ExtractFormat.HTML) if first and last and html.startswith("<p>"): if first and last and html.startswith("<p>"): # TODO brittle html = html[3:-4] html = html[3:-4] if html.startswith("<li") and (first or not parts[-1].startswith("<li")): if html.startswith("<li") and (first or not parts[-1].startswith("<li")): Loading @@ -967,7 +983,7 @@ class TS2MDExtractor: return (filename, width_emu). Otherwise return (None, None). return (filename, width_emu). Otherwise return (None, None). """ """ if i > 10: if i > 10: return None, 0 return None, 0, None if isinstance(paragraph, Paragraph): if isinstance(paragraph, Paragraph): for run in paragraph.runs: for run in paragraph.runs: # An image inserted via add_picture() is stored as an inline shape # An image inserted via add_picture() is stored as an inline shape Loading @@ -992,7 +1008,19 @@ class TS2MDExtractor: # Original filename (as stored in the package, not necessarily original filesystem name) # Original filename (as stored in the package, not necessarily original filesystem name) filename = image_part.partname # e.g. '/word/media/image1.png' filename = image_part.partname # e.g. '/word/media/image1.png' return filename, width_emu # Cropping info (fractions of 1.0) crop = {} crop_attrs = run._element.xpath(".//pic:blipFill/a:srcRect") if crop_attrs: attrs = crop_attrs[0].attrib for key in ("l", "r", "t", "b"): if key in attrs: # Convert from 1/1000 percent to fraction crop[key] = int(attrs[key]) / 100000.0 else: crop = None return filename, width_emu, crop except Exception as ex: except Exception as ex: print(str(ex)) print(str(ex)) continue continue Loading @@ -1012,6 +1040,9 @@ class TS2MDExtractor: cap_id, cap_text = self.caption_from_prev_paragraph(table) cap_id, cap_text = self.caption_from_prev_paragraph(table) if cap_text and "Prefixes and namespaces" in cap_text: return "{{table_1}}" # Detect header cells by checking first row: if any cell is header, we'll render those cells as <th> # Detect header cells by checking first row: if any cell is header, we'll render those cells as <th> # (You can refine this if you want per-cell header detection per row.) # (You can refine this if you want per-cell header detection per row.) first_row = table.rows[0] if table.rows else None first_row = table.rows[0] if table.rows else None Loading @@ -1027,10 +1058,10 @@ class TS2MDExtractor: lines:List[str] = [] lines:List[str] = [] lines.append( lines.append( f"""<table id="{html.escape(cap_id)}" data-docx-preferred-width={preferred_width}>""" f"""<table data-docx-preferred-width={preferred_width}>""" ) ) if cap_id and cap_text: if cap_text: lines.append(f" <caption>{cap_text}</caption>") lines.append(f" <caption>{cap_text}</caption>") seen = set() seen = set() Loading src/saref_pypeline/docgen/ts_generator.py +125 −88 Original line number Original line Diff line number Diff line Loading @@ -46,7 +46,7 @@ from saref_pypeline.docgen.utils import ( EntityDescription, EntityDescription, with_flags, with_flags, materialize_links, materialize_links, pprint_xml pprint_xml, ) ) from saref_pypeline.entities import ( from saref_pypeline.entities import ( SAREFCore, SAREFCore, Loading Loading @@ -153,6 +153,7 @@ def set_table_borders(tbl: CT_Tbl): tbl_borders.append(border_elm) tbl_borders.append(border_elm) tbl_pr.append(tbl_borders) tbl_pr.append(tbl_borders) def strip_outer_text(el: Tag): def strip_outer_text(el: Tag): # lstrip on first child if it's a text node # lstrip on first child if it's a text node if el.contents and isinstance(el.contents[0], NavigableString): if el.contents and isinstance(el.contents[0], NavigableString): Loading @@ -162,6 +163,7 @@ def strip_outer_text(el:Tag): if el.contents and isinstance(el.contents[-1], NavigableString): if el.contents and isinstance(el.contents[-1], NavigableString): el.contents[-1].replace_with(el.contents[-1].rstrip()) el.contents[-1].replace_with(el.contents[-1].rstrip()) class Bookmark: class Bookmark: def __init__(self, outer: "TSGenerator", name: str): def __init__(self, outer: "TSGenerator", name: str): self._outer = outer self._outer = outer Loading Loading @@ -213,7 +215,6 @@ class TSGenerator: self.description(OWL.bottomDataProperty, OWL_GRAPH) self.description(OWL.bottomDataProperty, OWL_GRAPH) self.description(OWL.topDataProperty, OWL_GRAPH) self.description(OWL.topDataProperty, OWL_GRAPH) @staticmethod @staticmethod def _add_gridspan(cell: _Cell, val: int): def _add_gridspan(cell: _Cell, val: int): tcPr = cell._tc.get_or_add_tcPr() tcPr = cell._tc.get_or_add_tcPr() Loading Loading @@ -294,36 +295,38 @@ class TSGenerator: return self.document return self.document def ensure_cursor_paragraph_or_table(self) -> None: def ensure_cursor_paragraph(self, table_ok: bool = False) -> bool: """ Ensure the cursor is a paragraph or a table. - If the cursor is a Run → move cursor to its ancestor Paragraph. """ if isinstance(self._cursor, Run): while not isinstance(self._cursor, Paragraph | Table): self._cursor = self._cursor._parent if isinstance(self._cursor._parent, Table): self._cursor = self._cursor._parent def ensure_cursor_paragraph(self) -> None: """ """ Ensure the cursor is a paragraph. Ensure the cursor is a paragraph. - If the cursor is a Run → move cursor to its ancestor Paragraph. - If the cursor is a Run → move cursor to its ancestor Paragraph. - If the cursor is a Table → move cursor to a new paragraph. - If the cursor is a Table → move cursor to a new paragraph. - If the cursor is a Cell_ → move cursor to a new paragraph in the cell. Returns: bool: if a new paragraph has been created """ """ if isinstance(self._cursor, Paragraph): return False if isinstance(self._cursor, Run): if isinstance(self._cursor, Run): while not isinstance(self._cursor, Paragraph): while not isinstance(self._cursor, Paragraph): self._cursor = self._cursor._parent self._cursor = self._cursor._parent return False elif isinstance(self._cursor, Table): elif isinstance(self._cursor, Table): self.new_paragraph() if table_ok: return False new_p = OxmlElement("w:p") self._cursor._element.addnext(new_p) paragraph = Paragraph(new_p, self._cursor._parent) self._cursor = paragraph return True elif isinstance(self._cursor, _Cell): elif isinstance(self._cursor, _Cell): new_p = OxmlElement("w:p") new_p = OxmlElement("w:p") self._cursor._element.append(new_p) self._cursor._element.append(new_p) paragraph = Paragraph(new_p, self._cursor) paragraph = Paragraph(new_p, self._cursor) self._cursor = paragraph self._cursor = paragraph return True raise ValueError() def ensure_cursor_run(self) -> None: def ensure_cursor_run(self) -> None: """ """ Loading @@ -332,10 +335,7 @@ class TSGenerator: - If the cursor is a Paragraph → move cursor to a newly created run. - If the cursor is a Paragraph → move cursor to a newly created run. - If the cursor is a Table → move cursor to a newly created run in a newly created paragraph. - If the cursor is a Table → move cursor to a newly created run in a newly created paragraph. """ """ if isinstance(self._cursor, Paragraph): self.ensure_cursor_paragraph() self._cursor = self._cursor.add_run() elif isinstance(self._cursor, Table): self.new_paragraph() self._cursor = self._cursor.add_run() self._cursor = self._cursor.add_run() def ensure_pstyle(self, style: P_STYLE, styling: Callable = None) -> Callable: def ensure_pstyle(self, style: P_STYLE, styling: Callable = None) -> Callable: Loading Loading @@ -474,7 +474,9 @@ class TSGenerator: style: P_STYLE | str | None = None, style: P_STYLE | str | None = None, ) -> None: ) -> None: """ """ Insert a new paragraph immediately after the current cursor. Insert a new paragraph. - immediately after, if the cursor is a paragraph or a table. - in first position, if the cursor is a cell. Parameters Parameters ---------- ---------- Loading @@ -484,8 +486,10 @@ class TSGenerator: text : str, optional text : str, optional Text content of the new paragraph. If None, the paragraph is empty. Text content of the new paragraph. If None, the paragraph is empty. """ """ self.ensure_cursor_paragraph_or_table() is_new = self.ensure_cursor_paragraph() if is_new: paragraph = self._cursor else: new_p = OxmlElement("w:p") new_p = OxmlElement("w:p") self._cursor._element.addnext(new_p) self._cursor._element.addnext(new_p) paragraph = Paragraph(new_p, self._cursor._parent) paragraph = Paragraph(new_p, self._cursor._parent) Loading Loading @@ -514,7 +518,7 @@ class TSGenerator: parent block container. parent block container. """ """ self.ensure_cursor_paragraph_or_table() is_new = self.ensure_cursor_paragraph() width = width or self.get_block_width() width = width or self.get_block_width() tbl = CT_Tbl.new_tbl(rows, cols, width) tbl = CT_Tbl.new_tbl(rows, cols, width) Loading @@ -523,6 +527,11 @@ class TSGenerator: table = Table(tbl, self._cursor._parent) table = Table(tbl, self._cursor._parent) for arg_name in kwargs: for arg_name in kwargs: setattr(table, arg_name, kwargs[arg_name]) setattr(table, arg_name, kwargs[arg_name]) if is_new: # delete the paragraph that was newly created self._cursor.parent._element.remove(self._cursor._element) self._cursor = table self._cursor = table set_table_borders(tbl) set_table_borders(tbl) Loading Loading @@ -596,7 +605,7 @@ class TSGenerator: [] [] If no numbered heading is found before or at the cursor position. If no numbered heading is found before or at the cursor position. """ """ self.ensure_cursor_paragraph() self.ensure_cursor_paragraph(table_ok=True) # Get the document's paragraphs # Get the document's paragraphs all_block_items = list(iter_block_items(self.document)) all_block_items = list(iter_block_items(self.document)) # Find the index of the current paragraph # Find the index of the current paragraph Loading Loading @@ -1018,7 +1027,8 @@ class TSGenerator: if mandatory: if mandatory: self.new_paragraph() self.new_paragraph() self.new_run( self.new_run( f"File documentation/{file}.md does not exist.", style=C_STYLE.Guidance f"File documentation/{file}.md does not exist.", style=C_STYLE.Guidance, ) ) else: else: self.insert_soup(soup) self.insert_soup(soup) Loading Loading @@ -1067,7 +1077,8 @@ class TSGenerator: data = html_path.read_text(encoding="utf-8") data = html_path.read_text(encoding="utf-8") elif md_path.exists(): elif md_path.exists(): data = markdown( data = markdown( md_path.read_text(encoding="utf-8"), extensions=["extra", "admonition", "codehilite"] md_path.read_text(encoding="utf-8"), extensions=["extra", "admonition", "codehilite"], ) ) if not data: if not data: Loading @@ -1084,7 +1095,7 @@ class TSGenerator: self.insert_soup_children(el, styling) self.insert_soup_children(el, styling) elif isinstance(el, NavigableString): elif isinstance(el, NavigableString): if el == "\n": if not el or el == "\n": return return self.ensure_cursor_paragraph() self.ensure_cursor_paragraph() if styling: if styling: Loading Loading @@ -1133,29 +1144,27 @@ class TSGenerator: self.insert_soup_p(p, self.ensure_pstyle(P_STYLE.EW, styling)) self.insert_soup_p(p, self.ensure_pstyle(P_STYLE.EW, styling)) else: else: self.new_paragraph() self.new_paragraph() if styling: self.apply_styles(el, styling) styling(el) self.insert_soup_children(el, styling) self.insert_soup_children(el, styling) def apply_styles(self, el: Tag, styling: Callable = None) -> None: if styling: styling(el) if isinstance(self._cursor, Paragraph): if style := el.get("data-docx-pstyle", None): self._cursor.style = style if isinstance(self._cursor, Run): if style := el.get("data-docx-cstyle", None): self._cursor.style = style def insert_soup_br(self, el: Tag, styling: Callable = None) -> None: def insert_soup_br(self, el: Tag, styling: Callable = None) -> None: self.ensure_cursor_paragraph() self.new_paragraph() self.new_paragraph() if styling: self.apply_styles(el, styling) styling() def insert_soup_p(self, el: Tag, styling: Callable = None) -> None: def insert_soup_p(self, el: Tag, styling: Callable = None) -> None: self.ensure_cursor_paragraph() if any(parent.name == "table" for parent in el.parents) \ and len(self._cursor._p.getparent().xpath("./w:p")) == 1 \ and len(self._cursor._p.getparent().xpath("./w:p/w:r")) == 0: self.ensure_cursor_paragraph() # do nothing else: self.new_paragraph() self.new_paragraph() if styling: self.apply_styles(el, styling) styling(el) add_tab = False add_tab = False if el.getText().startswith("NOTE"): if el.getText().startswith("NOTE"): Loading Loading @@ -1231,12 +1240,8 @@ class TSGenerator: def insert_soup_li(self, li: Tag, styling: Callable = None) -> None: def insert_soup_li(self, li: Tag, styling: Callable = None) -> None: self.new_paragraph() self.new_paragraph() if styling: self.apply_styles(li, styling) styling(li) style = self._cursor.style style = self._cursor.style if "data-docx-pstyle" in li.attrs: style = li.get("data-docx-pstyle") styling = self.ensure_pstyle(style, styling) for child in li.children: for child in li.children: if isinstance(child, Tag) and child.name == "ul": if isinstance(child, Tag) and child.name == "ul": Loading Loading @@ -1374,7 +1379,30 @@ class TSGenerator: # Add paragraph for image # Add paragraph for image self.new_paragraph(style=P_STYLE.FL) self.new_paragraph(style=P_STYLE.FL) self.new_run() self.new_run() self._cursor.add_picture(str(img_path), width=picture_width) shape = self._cursor.add_picture(str(img_path), width=picture_width) crop_l = int(100000.0 * float(el.get("data-docx-crop-l", "0"))) crop_t = int(100000.0 * float(el.get("data-docx-crop-t", "0"))) crop_r = int(100000.0 * float(el.get("data-docx-crop-r", "0"))) crop_b = int(100000.0 * float(el.get("data-docx-crop-b", "0"))) if any([crop_l, crop_t, crop_r, crop_b]): blipfill = shape._inline.xpath(".//pic:blipFill") if blipfill: blipfill = blipfill[0] # Remove any existing <a:srcRect> for sr in blipfill.xpath("./a:srcRect"): sr.getparent().remove(sr) src_rect = OxmlElement("a:srcRect") if crop_l: src_rect.set("l", str(crop_l)) if crop_t: src_rect.set("t", str(crop_t)) if crop_r: src_rect.set("r", str(crop_r)) if crop_b: src_rect.set("b", str(crop_b)) blipfill.insert(1, src_rect) # place after <a:blip> def insert_soup_figcaption(self, el: Tag, styling: Callable = None) -> None: def insert_soup_figcaption(self, el: Tag, styling: Callable = None) -> None: """ """ Loading @@ -1399,7 +1427,9 @@ class TSGenerator: else: else: return self.insert_soup_children(a, styling) return self.insert_soup_children(a, styling) def insert_soup_hyperlink(self, text: str, url: str, styling: Callable = None) -> None: def insert_soup_hyperlink( self, text: str, url: str, styling: Callable = None ) -> None: # Create the relationship in the document for the hyperlink # Create the relationship in the document for the hyperlink part = self._cursor.part part = self._cursor.part r_id = part.relate_to(url, RT.HYPERLINK, is_external=True) r_id = part.relate_to(url, RT.HYPERLINK, is_external=True) Loading @@ -1426,7 +1456,9 @@ class TSGenerator: h.append(r) h.append(r) self._cursor._element.append(h) self._cursor._element.append(h) def insert_soup_internal_hyperlink(self, text: str, anchor_name: str, styling: Callable = None) -> None: def insert_soup_internal_hyperlink( self, text: str, anchor_name: str, styling: Callable = None ) -> None: # special case # special case if anchor_name == "#[0]": if anchor_name == "#[0]": self.new_run("the present document") self.new_run("the present document") Loading Loading @@ -1502,7 +1534,9 @@ class TSGenerator: with Bookmark(self, id): with Bookmark(self, id): self.insert_soup_children(el, styling) self.insert_soup_children(el, styling) def insert_soup_tr(self, el: Tag, spans:List[int], table: Table, styling: Callable = None) -> List[int]: def insert_soup_tr( self, el: Tag, spans: List[int], table: Table, styling: Callable = None ) -> List[int]: """ """ Add a <tr> (table row) to a python-docx Table. Add a <tr> (table row) to a python-docx Table. """ """ Loading Loading @@ -1571,16 +1605,16 @@ class TSGenerator: Add a <td> (table data cell). Add a <td> (table data cell). """ """ cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER # for _p in cell._tc.xpath("./w:p"): # cell._element.remove(_p) # remove default paragraph # self._cursor = cell cell._element.remove(cell.paragraphs[0]._element) self._cursor = cell.paragraphs[0] self._cursor = cell # Decide style based on HTML attributes # Decide style based on HTML attributes style = el.get("style", "").lower() style = el.get("style", "").lower() styles = dict( styles = dict( rule.strip().split(":", 1) rule.strip().split(":", 1) for rule in style.split(";") if ":" in rule for rule in style.split(";") if ":" in rule ) ) align = styles.get("text-align", "").strip() align = styles.get("text-align", "").strip() if align == "center": if align == "center": Loading @@ -1591,6 +1625,10 @@ class TSGenerator: style = P_STYLE.TAL # default align left style = P_STYLE.TAL # default align left self.insert_soup_children(el, self.ensure_pstyle(style, styling)) self.insert_soup_children(el, self.ensure_pstyle(style, styling)) # ensure every cell has a default paragraph if not cell.paragraphs: cell.add_paragraph() # --------------------------------------------------------------------- # --------------------------------------------------------------------- # Methods for the ontology reference annex # Methods for the ontology reference annex # --------------------------------------------------------------------- # --------------------------------------------------------------------- Loading Loading @@ -1995,7 +2033,9 @@ class TSGenerator: elif literal.datatype == URIRef( elif literal.datatype == URIRef( "http://www.iana.org/assignments/media-types/text/markdown" "http://www.iana.org/assignments/media-types/text/markdown" ): ): self.insert_soup(markdown(literal, extensions=["extra", "admonition", "codehilite"])) self.insert_soup( markdown(literal, extensions=["extra", "admonition", "codehilite"]) ) else: else: self.new_run(literal.replace("\r", "")) self.new_run(literal.replace("\r", "")) Loading Loading @@ -2152,9 +2192,6 @@ class TSGenerator: (OWL.oneOf, "<span class='logic'>one of</span> { ", ", ", " }", True), (OWL.oneOf, "<span class='logic'>one of</span> { ", ", ", " }", True), ]: ]: if l := next(g.objects(entity, property), None): if l := next(g.objects(entity, property), None): logger.debug( "AXIOM data range intersection, union, and literal enumeration" ) self.new_run(first) self.new_run(first) while l != RDF.nil: while l != RDF.nil: first = next(g.objects(l, RDF.first), None) first = next(g.objects(l, RDF.first), None) Loading src/saref_pypeline/docgen/website_generator.py +0 −3 Original line number Original line Diff line number Diff line Loading @@ -1526,9 +1526,6 @@ class WebsiteGenerator: (OWL.oneOf, "<span class='logic'>one of</span> { ", ", ", " }", True), (OWL.oneOf, "<span class='logic'>one of</span> { ", ", ", " }", True), ]: ]: if l := next(g.objects(entity, property), None): if l := next(g.objects(entity, property), None): logger.debug( "AXIOM data range intersection, union, and literal enumeration" ) raw(first) raw(first) while l != RDF.nil: while l != RDF.nil: first = next(g.objects(l, RDF.first), None) first = next(g.objects(l, RDF.first), None) Loading Loading
src/saref_pypeline/docgen/ts2md_extractor.py +47 −16 Original line number Original line Diff line number Diff line Loading @@ -409,8 +409,12 @@ class TS2MDExtractor: i += 1 i += 1 code_char = 65 # A code_char = 65 # A annexes_file = Path(self.out_folder, "annexes.md") annexes_file.write_text("", encoding="utf-8") while md := self.extract_clause(rf"^Annex {chr(code_char)} ", True): while md := self.extract_clause(rf"^Annex {chr(code_char)} ", True): Path(self.out_folder, f"annex_{chr(code_char)}.md").write_text(md, "utf-8") with annexes_file.open("a", encoding="utf-8") as f: f.write(md) f.write("\n") code_char += 1 code_char += 1 logger.debug(f"Extraction complete: {self.out_folder}") logger.debug(f"Extraction complete: {self.out_folder}") Loading Loading @@ -516,6 +520,7 @@ class TS2MDExtractor: md_output = [] md_output = [] for block_item in iter_block_items(self.document, text, include_title): for block_item in iter_block_items(self.document, text, include_title): md = self.extract_block_item(block_item) md = self.extract_block_item(block_item) if md: md_output.append(md) md_output.append(md) return "\n".join(md_output) return "\n".join(md_output) Loading Loading @@ -684,9 +689,13 @@ class TS2MDExtractor: left, right = md.split("\t", 1) left, right = md.split("\t", 1) content = right.replace("\n", "\n ") content = right.replace("\n", "\n ") return f'!!! example "{left}"\n{content}\n' return f'!!! example "{left}"\n{content}\n' else: elif md.startswith("["): # Reference # Reference return f"* {md}" return f"* {md}" else: # last item of abbreviations: left, right = md.split("\t", 1) return f"* {left}: {right}" def extract_EW( def extract_EW( self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD Loading Loading @@ -772,6 +781,9 @@ class TS2MDExtractor: # exception if in table # exception if in table if paragraph._element.getparent().tag == qn("w:tc"): if paragraph._element.getparent().tag == qn("w:tc"): return self.extract_TB1(paragraph, extract_format) return self.extract_TB1(paragraph, extract_format) # return nothing if empty paragraph if not paragraph.text: return None return self.extract_B_plus(paragraph, 0, extract_format) return self.extract_B_plus(paragraph, 0, extract_format) def extract_B2_plus( def extract_B2_plus( Loading @@ -797,8 +809,8 @@ class TS2MDExtractor: self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD ): ): """Bulleted (letters) indent 1""" """Bulleted (letters) indent 1""" md = self.extract_inner_content(paragraph, extract_format) html = self.extract_inner_content(paragraph, ExtractFormat.HTML) return f"a. {md}" return f'<p data-docx-pstyle="BL">{html}</p>' # General styles For different items # General styles For different items Loading Loading @@ -846,10 +858,14 @@ class TS2MDExtractor: return "" return "" label, caption = match.group(1), match.group(2) label, caption = match.group(1), match.group(2) filename, width = self.get_paragraph_image_info(paragraph) filename, width, crop = self.get_paragraph_image_info(paragraph) if filename: if filename: return f"""<figure id="{label.replace(" ","_")}"> if crop: <img data-docx-width="{width/Cm(1):.2f}cm" src="diagrams/{os.path.basename(filename)}" alt="{caption}"/> crop_attr = " ".join(f' data-docx-crop-{k}="{v}"' for k,v in crop.items()) else: crop_attr = "" return f"""<figure> <img data-docx-width="{width/Cm(1):.2f}cm"{crop_attr} src="diagrams/{os.path.basename(filename)}" alt="{caption}"/> <figcaption>{label}: {caption}</figcaption> <figcaption>{label}: {caption}</figcaption> </figure>\n""" </figure>\n""" else: else: Loading Loading @@ -930,7 +946,7 @@ class TS2MDExtractor: return (caption_id, caption_text). Otherwise, return ("Table_id_unknown", "label_unknown"). return (caption_id, caption_text). Otherwise, return ("Table_id_unknown", "label_unknown"). """ """ if i > 3: if i > 3: return "Table_id_unknown", "label_unknown" return None, None prev = get_prev_block(self.document, table) prev = get_prev_block(self.document, table) if isinstance(prev, Paragraph) and getattr(prev.style, "name", None) == "TH": if isinstance(prev, Paragraph) and getattr(prev.style, "name", None) == "TH": md = self.extract_inner_content(prev) md = self.extract_inner_content(prev) Loading @@ -949,7 +965,7 @@ class TS2MDExtractor: parts = [] parts = [] for first, last, p in with_flags(cell.paragraphs): for first, last, p in with_flags(cell.paragraphs): html = self.extract_paragraph(p, ExtractFormat.HTML) html = self.extract_paragraph(p, ExtractFormat.HTML) if first and last and html.startswith("<p>"): if first and last and html.startswith("<p>"): # TODO brittle html = html[3:-4] html = html[3:-4] if html.startswith("<li") and (first or not parts[-1].startswith("<li")): if html.startswith("<li") and (first or not parts[-1].startswith("<li")): Loading @@ -967,7 +983,7 @@ class TS2MDExtractor: return (filename, width_emu). Otherwise return (None, None). return (filename, width_emu). Otherwise return (None, None). """ """ if i > 10: if i > 10: return None, 0 return None, 0, None if isinstance(paragraph, Paragraph): if isinstance(paragraph, Paragraph): for run in paragraph.runs: for run in paragraph.runs: # An image inserted via add_picture() is stored as an inline shape # An image inserted via add_picture() is stored as an inline shape Loading @@ -992,7 +1008,19 @@ class TS2MDExtractor: # Original filename (as stored in the package, not necessarily original filesystem name) # Original filename (as stored in the package, not necessarily original filesystem name) filename = image_part.partname # e.g. '/word/media/image1.png' filename = image_part.partname # e.g. '/word/media/image1.png' return filename, width_emu # Cropping info (fractions of 1.0) crop = {} crop_attrs = run._element.xpath(".//pic:blipFill/a:srcRect") if crop_attrs: attrs = crop_attrs[0].attrib for key in ("l", "r", "t", "b"): if key in attrs: # Convert from 1/1000 percent to fraction crop[key] = int(attrs[key]) / 100000.0 else: crop = None return filename, width_emu, crop except Exception as ex: except Exception as ex: print(str(ex)) print(str(ex)) continue continue Loading @@ -1012,6 +1040,9 @@ class TS2MDExtractor: cap_id, cap_text = self.caption_from_prev_paragraph(table) cap_id, cap_text = self.caption_from_prev_paragraph(table) if cap_text and "Prefixes and namespaces" in cap_text: return "{{table_1}}" # Detect header cells by checking first row: if any cell is header, we'll render those cells as <th> # Detect header cells by checking first row: if any cell is header, we'll render those cells as <th> # (You can refine this if you want per-cell header detection per row.) # (You can refine this if you want per-cell header detection per row.) first_row = table.rows[0] if table.rows else None first_row = table.rows[0] if table.rows else None Loading @@ -1027,10 +1058,10 @@ class TS2MDExtractor: lines:List[str] = [] lines:List[str] = [] lines.append( lines.append( f"""<table id="{html.escape(cap_id)}" data-docx-preferred-width={preferred_width}>""" f"""<table data-docx-preferred-width={preferred_width}>""" ) ) if cap_id and cap_text: if cap_text: lines.append(f" <caption>{cap_text}</caption>") lines.append(f" <caption>{cap_text}</caption>") seen = set() seen = set() Loading
src/saref_pypeline/docgen/ts_generator.py +125 −88 Original line number Original line Diff line number Diff line Loading @@ -46,7 +46,7 @@ from saref_pypeline.docgen.utils import ( EntityDescription, EntityDescription, with_flags, with_flags, materialize_links, materialize_links, pprint_xml pprint_xml, ) ) from saref_pypeline.entities import ( from saref_pypeline.entities import ( SAREFCore, SAREFCore, Loading Loading @@ -153,6 +153,7 @@ def set_table_borders(tbl: CT_Tbl): tbl_borders.append(border_elm) tbl_borders.append(border_elm) tbl_pr.append(tbl_borders) tbl_pr.append(tbl_borders) def strip_outer_text(el: Tag): def strip_outer_text(el: Tag): # lstrip on first child if it's a text node # lstrip on first child if it's a text node if el.contents and isinstance(el.contents[0], NavigableString): if el.contents and isinstance(el.contents[0], NavigableString): Loading @@ -162,6 +163,7 @@ def strip_outer_text(el:Tag): if el.contents and isinstance(el.contents[-1], NavigableString): if el.contents and isinstance(el.contents[-1], NavigableString): el.contents[-1].replace_with(el.contents[-1].rstrip()) el.contents[-1].replace_with(el.contents[-1].rstrip()) class Bookmark: class Bookmark: def __init__(self, outer: "TSGenerator", name: str): def __init__(self, outer: "TSGenerator", name: str): self._outer = outer self._outer = outer Loading Loading @@ -213,7 +215,6 @@ class TSGenerator: self.description(OWL.bottomDataProperty, OWL_GRAPH) self.description(OWL.bottomDataProperty, OWL_GRAPH) self.description(OWL.topDataProperty, OWL_GRAPH) self.description(OWL.topDataProperty, OWL_GRAPH) @staticmethod @staticmethod def _add_gridspan(cell: _Cell, val: int): def _add_gridspan(cell: _Cell, val: int): tcPr = cell._tc.get_or_add_tcPr() tcPr = cell._tc.get_or_add_tcPr() Loading Loading @@ -294,36 +295,38 @@ class TSGenerator: return self.document return self.document def ensure_cursor_paragraph_or_table(self) -> None: def ensure_cursor_paragraph(self, table_ok: bool = False) -> bool: """ Ensure the cursor is a paragraph or a table. - If the cursor is a Run → move cursor to its ancestor Paragraph. """ if isinstance(self._cursor, Run): while not isinstance(self._cursor, Paragraph | Table): self._cursor = self._cursor._parent if isinstance(self._cursor._parent, Table): self._cursor = self._cursor._parent def ensure_cursor_paragraph(self) -> None: """ """ Ensure the cursor is a paragraph. Ensure the cursor is a paragraph. - If the cursor is a Run → move cursor to its ancestor Paragraph. - If the cursor is a Run → move cursor to its ancestor Paragraph. - If the cursor is a Table → move cursor to a new paragraph. - If the cursor is a Table → move cursor to a new paragraph. - If the cursor is a Cell_ → move cursor to a new paragraph in the cell. Returns: bool: if a new paragraph has been created """ """ if isinstance(self._cursor, Paragraph): return False if isinstance(self._cursor, Run): if isinstance(self._cursor, Run): while not isinstance(self._cursor, Paragraph): while not isinstance(self._cursor, Paragraph): self._cursor = self._cursor._parent self._cursor = self._cursor._parent return False elif isinstance(self._cursor, Table): elif isinstance(self._cursor, Table): self.new_paragraph() if table_ok: return False new_p = OxmlElement("w:p") self._cursor._element.addnext(new_p) paragraph = Paragraph(new_p, self._cursor._parent) self._cursor = paragraph return True elif isinstance(self._cursor, _Cell): elif isinstance(self._cursor, _Cell): new_p = OxmlElement("w:p") new_p = OxmlElement("w:p") self._cursor._element.append(new_p) self._cursor._element.append(new_p) paragraph = Paragraph(new_p, self._cursor) paragraph = Paragraph(new_p, self._cursor) self._cursor = paragraph self._cursor = paragraph return True raise ValueError() def ensure_cursor_run(self) -> None: def ensure_cursor_run(self) -> None: """ """ Loading @@ -332,10 +335,7 @@ class TSGenerator: - If the cursor is a Paragraph → move cursor to a newly created run. - If the cursor is a Paragraph → move cursor to a newly created run. - If the cursor is a Table → move cursor to a newly created run in a newly created paragraph. - If the cursor is a Table → move cursor to a newly created run in a newly created paragraph. """ """ if isinstance(self._cursor, Paragraph): self.ensure_cursor_paragraph() self._cursor = self._cursor.add_run() elif isinstance(self._cursor, Table): self.new_paragraph() self._cursor = self._cursor.add_run() self._cursor = self._cursor.add_run() def ensure_pstyle(self, style: P_STYLE, styling: Callable = None) -> Callable: def ensure_pstyle(self, style: P_STYLE, styling: Callable = None) -> Callable: Loading Loading @@ -474,7 +474,9 @@ class TSGenerator: style: P_STYLE | str | None = None, style: P_STYLE | str | None = None, ) -> None: ) -> None: """ """ Insert a new paragraph immediately after the current cursor. Insert a new paragraph. - immediately after, if the cursor is a paragraph or a table. - in first position, if the cursor is a cell. Parameters Parameters ---------- ---------- Loading @@ -484,8 +486,10 @@ class TSGenerator: text : str, optional text : str, optional Text content of the new paragraph. If None, the paragraph is empty. Text content of the new paragraph. If None, the paragraph is empty. """ """ self.ensure_cursor_paragraph_or_table() is_new = self.ensure_cursor_paragraph() if is_new: paragraph = self._cursor else: new_p = OxmlElement("w:p") new_p = OxmlElement("w:p") self._cursor._element.addnext(new_p) self._cursor._element.addnext(new_p) paragraph = Paragraph(new_p, self._cursor._parent) paragraph = Paragraph(new_p, self._cursor._parent) Loading Loading @@ -514,7 +518,7 @@ class TSGenerator: parent block container. parent block container. """ """ self.ensure_cursor_paragraph_or_table() is_new = self.ensure_cursor_paragraph() width = width or self.get_block_width() width = width or self.get_block_width() tbl = CT_Tbl.new_tbl(rows, cols, width) tbl = CT_Tbl.new_tbl(rows, cols, width) Loading @@ -523,6 +527,11 @@ class TSGenerator: table = Table(tbl, self._cursor._parent) table = Table(tbl, self._cursor._parent) for arg_name in kwargs: for arg_name in kwargs: setattr(table, arg_name, kwargs[arg_name]) setattr(table, arg_name, kwargs[arg_name]) if is_new: # delete the paragraph that was newly created self._cursor.parent._element.remove(self._cursor._element) self._cursor = table self._cursor = table set_table_borders(tbl) set_table_borders(tbl) Loading Loading @@ -596,7 +605,7 @@ class TSGenerator: [] [] If no numbered heading is found before or at the cursor position. If no numbered heading is found before or at the cursor position. """ """ self.ensure_cursor_paragraph() self.ensure_cursor_paragraph(table_ok=True) # Get the document's paragraphs # Get the document's paragraphs all_block_items = list(iter_block_items(self.document)) all_block_items = list(iter_block_items(self.document)) # Find the index of the current paragraph # Find the index of the current paragraph Loading Loading @@ -1018,7 +1027,8 @@ class TSGenerator: if mandatory: if mandatory: self.new_paragraph() self.new_paragraph() self.new_run( self.new_run( f"File documentation/{file}.md does not exist.", style=C_STYLE.Guidance f"File documentation/{file}.md does not exist.", style=C_STYLE.Guidance, ) ) else: else: self.insert_soup(soup) self.insert_soup(soup) Loading Loading @@ -1067,7 +1077,8 @@ class TSGenerator: data = html_path.read_text(encoding="utf-8") data = html_path.read_text(encoding="utf-8") elif md_path.exists(): elif md_path.exists(): data = markdown( data = markdown( md_path.read_text(encoding="utf-8"), extensions=["extra", "admonition", "codehilite"] md_path.read_text(encoding="utf-8"), extensions=["extra", "admonition", "codehilite"], ) ) if not data: if not data: Loading @@ -1084,7 +1095,7 @@ class TSGenerator: self.insert_soup_children(el, styling) self.insert_soup_children(el, styling) elif isinstance(el, NavigableString): elif isinstance(el, NavigableString): if el == "\n": if not el or el == "\n": return return self.ensure_cursor_paragraph() self.ensure_cursor_paragraph() if styling: if styling: Loading Loading @@ -1133,29 +1144,27 @@ class TSGenerator: self.insert_soup_p(p, self.ensure_pstyle(P_STYLE.EW, styling)) self.insert_soup_p(p, self.ensure_pstyle(P_STYLE.EW, styling)) else: else: self.new_paragraph() self.new_paragraph() if styling: self.apply_styles(el, styling) styling(el) self.insert_soup_children(el, styling) self.insert_soup_children(el, styling) def apply_styles(self, el: Tag, styling: Callable = None) -> None: if styling: styling(el) if isinstance(self._cursor, Paragraph): if style := el.get("data-docx-pstyle", None): self._cursor.style = style if isinstance(self._cursor, Run): if style := el.get("data-docx-cstyle", None): self._cursor.style = style def insert_soup_br(self, el: Tag, styling: Callable = None) -> None: def insert_soup_br(self, el: Tag, styling: Callable = None) -> None: self.ensure_cursor_paragraph() self.new_paragraph() self.new_paragraph() if styling: self.apply_styles(el, styling) styling() def insert_soup_p(self, el: Tag, styling: Callable = None) -> None: def insert_soup_p(self, el: Tag, styling: Callable = None) -> None: self.ensure_cursor_paragraph() if any(parent.name == "table" for parent in el.parents) \ and len(self._cursor._p.getparent().xpath("./w:p")) == 1 \ and len(self._cursor._p.getparent().xpath("./w:p/w:r")) == 0: self.ensure_cursor_paragraph() # do nothing else: self.new_paragraph() self.new_paragraph() if styling: self.apply_styles(el, styling) styling(el) add_tab = False add_tab = False if el.getText().startswith("NOTE"): if el.getText().startswith("NOTE"): Loading Loading @@ -1231,12 +1240,8 @@ class TSGenerator: def insert_soup_li(self, li: Tag, styling: Callable = None) -> None: def insert_soup_li(self, li: Tag, styling: Callable = None) -> None: self.new_paragraph() self.new_paragraph() if styling: self.apply_styles(li, styling) styling(li) style = self._cursor.style style = self._cursor.style if "data-docx-pstyle" in li.attrs: style = li.get("data-docx-pstyle") styling = self.ensure_pstyle(style, styling) for child in li.children: for child in li.children: if isinstance(child, Tag) and child.name == "ul": if isinstance(child, Tag) and child.name == "ul": Loading Loading @@ -1374,7 +1379,30 @@ class TSGenerator: # Add paragraph for image # Add paragraph for image self.new_paragraph(style=P_STYLE.FL) self.new_paragraph(style=P_STYLE.FL) self.new_run() self.new_run() self._cursor.add_picture(str(img_path), width=picture_width) shape = self._cursor.add_picture(str(img_path), width=picture_width) crop_l = int(100000.0 * float(el.get("data-docx-crop-l", "0"))) crop_t = int(100000.0 * float(el.get("data-docx-crop-t", "0"))) crop_r = int(100000.0 * float(el.get("data-docx-crop-r", "0"))) crop_b = int(100000.0 * float(el.get("data-docx-crop-b", "0"))) if any([crop_l, crop_t, crop_r, crop_b]): blipfill = shape._inline.xpath(".//pic:blipFill") if blipfill: blipfill = blipfill[0] # Remove any existing <a:srcRect> for sr in blipfill.xpath("./a:srcRect"): sr.getparent().remove(sr) src_rect = OxmlElement("a:srcRect") if crop_l: src_rect.set("l", str(crop_l)) if crop_t: src_rect.set("t", str(crop_t)) if crop_r: src_rect.set("r", str(crop_r)) if crop_b: src_rect.set("b", str(crop_b)) blipfill.insert(1, src_rect) # place after <a:blip> def insert_soup_figcaption(self, el: Tag, styling: Callable = None) -> None: def insert_soup_figcaption(self, el: Tag, styling: Callable = None) -> None: """ """ Loading @@ -1399,7 +1427,9 @@ class TSGenerator: else: else: return self.insert_soup_children(a, styling) return self.insert_soup_children(a, styling) def insert_soup_hyperlink(self, text: str, url: str, styling: Callable = None) -> None: def insert_soup_hyperlink( self, text: str, url: str, styling: Callable = None ) -> None: # Create the relationship in the document for the hyperlink # Create the relationship in the document for the hyperlink part = self._cursor.part part = self._cursor.part r_id = part.relate_to(url, RT.HYPERLINK, is_external=True) r_id = part.relate_to(url, RT.HYPERLINK, is_external=True) Loading @@ -1426,7 +1456,9 @@ class TSGenerator: h.append(r) h.append(r) self._cursor._element.append(h) self._cursor._element.append(h) def insert_soup_internal_hyperlink(self, text: str, anchor_name: str, styling: Callable = None) -> None: def insert_soup_internal_hyperlink( self, text: str, anchor_name: str, styling: Callable = None ) -> None: # special case # special case if anchor_name == "#[0]": if anchor_name == "#[0]": self.new_run("the present document") self.new_run("the present document") Loading Loading @@ -1502,7 +1534,9 @@ class TSGenerator: with Bookmark(self, id): with Bookmark(self, id): self.insert_soup_children(el, styling) self.insert_soup_children(el, styling) def insert_soup_tr(self, el: Tag, spans:List[int], table: Table, styling: Callable = None) -> List[int]: def insert_soup_tr( self, el: Tag, spans: List[int], table: Table, styling: Callable = None ) -> List[int]: """ """ Add a <tr> (table row) to a python-docx Table. Add a <tr> (table row) to a python-docx Table. """ """ Loading Loading @@ -1571,16 +1605,16 @@ class TSGenerator: Add a <td> (table data cell). Add a <td> (table data cell). """ """ cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER # for _p in cell._tc.xpath("./w:p"): # cell._element.remove(_p) # remove default paragraph # self._cursor = cell cell._element.remove(cell.paragraphs[0]._element) self._cursor = cell.paragraphs[0] self._cursor = cell # Decide style based on HTML attributes # Decide style based on HTML attributes style = el.get("style", "").lower() style = el.get("style", "").lower() styles = dict( styles = dict( rule.strip().split(":", 1) rule.strip().split(":", 1) for rule in style.split(";") if ":" in rule for rule in style.split(";") if ":" in rule ) ) align = styles.get("text-align", "").strip() align = styles.get("text-align", "").strip() if align == "center": if align == "center": Loading @@ -1591,6 +1625,10 @@ class TSGenerator: style = P_STYLE.TAL # default align left style = P_STYLE.TAL # default align left self.insert_soup_children(el, self.ensure_pstyle(style, styling)) self.insert_soup_children(el, self.ensure_pstyle(style, styling)) # ensure every cell has a default paragraph if not cell.paragraphs: cell.add_paragraph() # --------------------------------------------------------------------- # --------------------------------------------------------------------- # Methods for the ontology reference annex # Methods for the ontology reference annex # --------------------------------------------------------------------- # --------------------------------------------------------------------- Loading Loading @@ -1995,7 +2033,9 @@ class TSGenerator: elif literal.datatype == URIRef( elif literal.datatype == URIRef( "http://www.iana.org/assignments/media-types/text/markdown" "http://www.iana.org/assignments/media-types/text/markdown" ): ): self.insert_soup(markdown(literal, extensions=["extra", "admonition", "codehilite"])) self.insert_soup( markdown(literal, extensions=["extra", "admonition", "codehilite"]) ) else: else: self.new_run(literal.replace("\r", "")) self.new_run(literal.replace("\r", "")) Loading Loading @@ -2152,9 +2192,6 @@ class TSGenerator: (OWL.oneOf, "<span class='logic'>one of</span> { ", ", ", " }", True), (OWL.oneOf, "<span class='logic'>one of</span> { ", ", ", " }", True), ]: ]: if l := next(g.objects(entity, property), None): if l := next(g.objects(entity, property), None): logger.debug( "AXIOM data range intersection, union, and literal enumeration" ) self.new_run(first) self.new_run(first) while l != RDF.nil: while l != RDF.nil: first = next(g.objects(l, RDF.first), None) first = next(g.objects(l, RDF.first), None) Loading
src/saref_pypeline/docgen/website_generator.py +0 −3 Original line number Original line Diff line number Diff line Loading @@ -1526,9 +1526,6 @@ class WebsiteGenerator: (OWL.oneOf, "<span class='logic'>one of</span> { ", ", ", " }", True), (OWL.oneOf, "<span class='logic'>one of</span> { ", ", ", " }", True), ]: ]: if l := next(g.objects(entity, property), None): if l := next(g.objects(entity, property), None): logger.debug( "AXIOM data range intersection, union, and literal enumeration" ) raw(first) raw(first) while l != RDF.nil: while l != RDF.nil: first = next(g.objects(l, RDF.first), None) first = next(g.objects(l, RDF.first), None) Loading