""" JRXML 元素自动排序 — 按 JasperReports XSD 要求重排子元素。 XSD 要求 jasperReport 子元素严格按以下顺序: property, propertyExpression, import, template, reportFont, style, subDataset, scriptlet, parameter, queryString, field, sortField, variable, filterExpression, group, background, title, pageHeader, columnHeader, detail, columnFooter, pageFooter, lastPageFooter, summary, noData 以及 band 内部的 reportElement 必须在其他元素之前。 """ import re import xml.etree.ElementTree as ET from typing import Optional # JasperReports XSD sequence 顺序(索引越小越靠前) JASPERREPORT_ORDER = { "property": 0, "propertyExpression": 1, "import": 2, "template": 3, "reportFont": 4, "style": 5, "subDataset": 6, "scriptlet": 7, "parameter": 8, "queryString": 9, "field": 10, "sortField": 11, "variable": 12, "filterExpression": 13, "group": 14, "background": 15, "title": 16, "pageHeader": 17, "columnHeader": 18, "detail": 19, "columnFooter": 20, "pageFooter": 21, "lastPageFooter": 22, "summary": 23, "noData": 24, } # 带命名空间的标签映射(去掉 ns 前缀后匹配) NS = "http://jasperreports.sourceforge.net/jasperreports" def _tag_local(tag: str) -> str: """提取标签本地名(去掉命名空间前缀)。""" return tag.split("}")[-1] if "}" in tag else tag def _sort_key(elem: ET.Element) -> int: """排序键:按 JASPERREPORT_ORDER 中的顺序,未知元素放最后。""" local = _tag_local(elem.tag) return JASPERREPORT_ORDER.get(local, 999) def reorder_jrxml_elements(xml_string: str) -> str: """重排 JRXML 字符串中的子元素顺序,使其符合 XSD sequence 要求。 处理范围: - jasperReport 的直接子元素 - band 的直接子元素(reportElement 在前) 返回重排后的 XML 字符串。如果解析失败,返回原始字符串。 """ try: root = ET.fromstring(xml_string) except ET.ParseError: return xml_string # 无法解析,返回原始 _reorder_children(root) _reorder_bands(root) # 序列化回字符串 result = ET.tostring(root, encoding="unicode") # 恢复 XML 声明、CDATA、命名空间 result = _restore_formatting(xml_string, result) return result def _reorder_children(parent: ET.Element): """递归重排所有子元素。""" children = list(parent) if not children: return # 按 XSD 顺序排序 children.sort(key=_sort_key) # 重建子元素列表 for i, child in enumerate(children): # ET 不支持直接 reorder,用 remove + insert pass # 实际上 ElementTree 不支持直接重排,需要重建 # 我们用更可靠的方式:收集所有子元素,清空,再按顺序添加 sorted_children = sorted(list(parent), key=_sort_key) # 移除所有子元素 for child in list(parent): parent.remove(child) # 按排序后的顺序重新添加(保持 tail 文本在最后) tail_text = "" for child in sorted_children: tail_text = child.tail or "" child.tail = "" parent.append(child) # 恢复最后一个元素的 tail if sorted_children and tail_text: sorted_children[-1].tail = tail_text # 递归处理子元素 for child in parent: _reorder_children(child) def _reorder_bands(root: ET.Element): """确保 band 内部 reportElement 在其他元素之前。""" for elem in root.iter(): if _tag_local(elem.tag) == "band": _ensure_reportelement_first(elem) def _ensure_reportelement_first(band: ET.Element): """在 band 内部,确保 reportElement 元素排在最前面。""" children = list(band) report_elements = [c for c in children if _tag_local(c.tag) == "reportElement"] other_elements = [c for c in children if _tag_local(c.tag) != "reportElement"] if not report_elements: return # 移除所有 for c in list(band): band.remove(c) # 先添加 reportElement tail = "" for r in report_elements: r.tail = "" band.append(r) # 再添加其他 for o in other_elements: o.tail = "" band.append(o) # 恢复 tail last = band[-1] if list(band) else None if last and children: last.tail = children[-1].tail or "" def _restore_formatting(original: str, reordered: str) -> str: """恢复 XML 声明和 CDATA 段。""" # 保留原始声明 decl = "" if original.strip().startswith("', original) if m: decl = m.group() if decl and not reordered.strip().startswith("', re.DOTALL) cdata_blocks = cdata_pattern.findall(original) if cdata_blocks: # 在重排后的 XML 中,对应位置的文本用 CDATA 包裹 def _restore_cdata(match): nonlocal cdata_blocks text = match.group(1) for cdata in cdata_blocks: if cdata.strip() == text.strip(): return f"" return match.group(0) # 替换已转义的文本为 CDATA reordered = re.sub( r'(]*>)\s*(.*?)\s*()', lambda m: m.group(1) + f"\n \n " + m.group(3), reordered, flags=re.DOTALL ) return reordered def normalize_jrxml(jrxml_text: str) -> str: """规范化 JRXML:排序元素 + 恢复格式。""" if not jrxml_text or not jrxml_text.strip(): return jrxml_text result = reorder_jrxml_elements(jrxml_text) return result