|
# SPDX-License-Identifier: Apache-2.0 |
|
# |
|
# based on broken code generated by ChatGPT |
|
|
|
import argparse |
|
import os |
|
from lxml import etree |
|
|
|
whitespace = " " |
|
|
|
def parse_with_comments(xml_file_path): |
|
"""Parses the XML and associates comments directly with elements.""" |
|
with open(xml_file_path, 'rb') as f: |
|
xml_string = f.read() |
|
parser = etree.XMLParser(remove_blank_text=True) |
|
root = etree.fromstring(xml_string, parser=parser) |
|
elements_and_associated_comments = [] |
|
previous_comment = None |
|
|
|
for child in root: |
|
if isinstance(child, etree._Comment): |
|
previous_comment = child |
|
else: |
|
elements_and_associated_comments.append((child, previous_comment)) |
|
previous_comment = None |
|
|
|
return root.xpath("/comment()"), elements_and_associated_comments |
|
|
|
def parse_with_comments_maybe_folder(xml_file_path): |
|
if os.path.isdir(xml_file_path): |
|
elements_and_comments_1 = [] |
|
top_level_comments_1 = [] |
|
for file_name in os.listdir(xml_file_path): |
|
if file_name.endswith('.xml') and file_name != "public.xml" and file_name != "symbols.xml": |
|
file_path = os.path.join(xml_file_path, file_name) |
|
top_level_comments, elements_and_comments = parse_with_comments(file_path) |
|
top_level_comments_1.extend(top_level_comments) |
|
elements_and_comments_1.extend(elements_and_comments) |
|
elif os.path.isfile(xml_file_path): |
|
top_level_comments_1, elements_and_comments_1 = parse_with_comments(xml_file_path) |
|
else: |
|
raise FileNotFoundError(f"The specified XML input '{xml_file_path}' does not exist.") |
|
return top_level_comments_1, elements_and_comments_1 |
|
|
|
def sort_and_copy_comments(xml_input_1, xml_file_2, output_file): |
|
# Extract elements and comments from XML 2 |
|
_, elements_and_comments_2 = parse_with_comments_maybe_folder(xml_file_2) |
|
|
|
# Create a mapping of element names to their original order in XML 2 |
|
order_in_2 = { |
|
elem.attrib['name']: idx |
|
for idx, (elem, _) in enumerate(elements_and_comments_2) |
|
if isinstance(elem, etree._Element) and 'name' in elem.attrib |
|
} |
|
|
|
# Combine elements and comments from XML 1 files |
|
top_level_comments_1, elements_and_comments_1 = parse_with_comments_maybe_folder(xml_input_1) |
|
|
|
# Sort the elements from XML 1 based on the order in XML 2 |
|
sorted_elements = sorted( |
|
[(e, c) for e, c in elements_and_comments_1 if isinstance(e, etree._Element)], |
|
key=lambda x: order_in_2.get(x[0].attrib.get('name', ''), float('inf')) |
|
) |
|
|
|
# Identify elements in XML 1 that are not present in XML 2 |
|
unsorted_elements = [ |
|
(e, c) for e, c in elements_and_comments_1 |
|
if isinstance(e, etree._Element) and e.attrib.get('name') not in order_in_2 |
|
] |
|
|
|
# Create a new XML structure with sorted elements and comments from XML 2 |
|
root = etree.Element('resources') |
|
top_level_comments_1.reverse() |
|
for comment in top_level_comments_1: |
|
comment.tail = "\n" |
|
root.addprevious(comment) |
|
|
|
for elem, xml2_comment in elements_and_comments_2: |
|
if isinstance(elem, etree._Element) and elem.attrib.get('name') in order_in_2: |
|
# Add sorted elements to the output |
|
for sorted_elem, xml1_comment in sorted_elements: |
|
if sorted_elem.attrib.get('name') == elem.attrib['name']: |
|
if xml1_comment is not None and len(xml1_comment.text) > 0: |
|
if xml2_comment is not None and len(xml2_comment.text) > 0: |
|
if xml1_comment.text != xml2_comment.text: |
|
print("WARNING: discarding input file comment for element that exists in base " |
|
"config.xml and has different comment there\n" |
|
f"Input file comment: {xml1_comment}\n" |
|
f"Base config file comment: {xml2_comment}\n") |
|
xml2_comment.tail = f"\n{whitespace}" |
|
root.append(xml2_comment) |
|
else: |
|
print(f"WARNING: copying over comment for element {elem.attrib['name']} that exists in base" |
|
f" config.xml but has no comment there\n") |
|
xml1_comment.tail = f"\n{whitespace}" |
|
root.append(xml1_comment) |
|
elif xml2_comment is not None and len(xml2_comment.text) > 0: |
|
xml2_comment.tail = f"\n{whitespace}" |
|
root.append(xml2_comment) |
|
|
|
# Copy the "translatable" attribute if present in XML 2 |
|
if 'translatable' in elem.attrib: |
|
if 'translatable' in sorted_elem.attrib and sorted_elem.attrib['translatable'] != elem.attrib['translatable']: |
|
print(f"WARNING: copying over translatable={elem.attrib['translatable']} from base " |
|
f"config.xml for element {elem.attrib['name']} that has set " |
|
f"translatable=\"{elem.attrib['translatable']}\" in input file\n") |
|
sorted_elem.attrib['translatable'] = elem.attrib['translatable'] |
|
etree.indent(sorted_elem, space=whitespace, level=1) |
|
sorted_elem.tail = f"\n\n{whitespace}" |
|
root.append(sorted_elem) |
|
|
|
for elem, xml1_comment in unsorted_elements: |
|
print(f"WARNING: Discarding element '{elem.attrib.get('name', '')}' not present in base config.xml") |
|
if xml1_comment is not None and len(xml1_comment.text) > 0: |
|
print(f"Comment: {xml1_comment}") |
|
print(f"Element: {etree.tostring(elem)}") |
|
print() |
|
|
|
if len(root) > 1: |
|
root.text = f"\n\n{whitespace}" |
|
root[-1].tail = "\n\n" |
|
tree = etree.ElementTree(root) |
|
tree.write(output_file, encoding='utf-8', xml_declaration=True) |
|
|
|
def main(): |
|
# Set up the argument parser |
|
parser = argparse.ArgumentParser(description="Sort the elements of XML files in a folder or a single XML file based on another XML file and copy comments.") |
|
parser.add_argument("xml_input_1", help="The folder or XML file containing XML data to sort.") |
|
parser.add_argument("xml_file_2", help="The second XML file that defines the sort order and provides comments.") |
|
parser.add_argument("output_file", help="The output file where the sorted XML will be saved.") |
|
|
|
# Parse arguments |
|
args = parser.parse_args() |
|
|
|
# Call the function with the provided arguments |
|
sort_and_copy_comments(args.xml_input_1, args.xml_file_2, args.output_file) |
|
|
|
if __name__ == "__main__": |
|
main() |