drmohundro / SWXMLHash

Simple XML parsing in Swift
MIT License
1.4k stars 203 forks source link

How to render XML nodes and its sub nodes in the exact order as they appear but from a deserialized Model? #275

Closed waelsaad closed 1 year ago

waelsaad commented 1 year ago

Hi,

I really need some help with this question as I have been struggeling with it on my own for some time and I 've been trying lots of different XML libraries but XMLHash is the one I've extensivly spent my time on.

I have 1000+ XML files that I am loading dynamically and the order of how the nodes and the sub nodes appear is important to perserve because each XML file will be turned into a readable article in a SwiftUI view.

I have already implemented a solution for my problem using recursive for loops using this approach to handle all the different types of nodes:

func enumerate(indexer: XMLIndexer) {
    for child in indexer.children {
        print(child.element!.name)
        enumerate(child)
    }
}

enumerate(indexer: xml)

However I created a completely deserializable model that covers all the possible scenarios of how the nodes and its sub nodes can appear and I would like to loop thru that deserialized object directly once we read the content of the XML file in a SwiftUI view and try to render it correctly.

Probablly in the same way if we were trying to render a HTML file to display all the nodes in the same order. I need to do the same for my XML files but from the deserializable object.

I am not sure what is the best approach to do this.

Please let me know if I haven't explained my problem properlly or you need me to paste code attempts. If you know of a solution please share. Thanks

Here is a simple version of a sample XML file structure but please keep in minde how the sub nodes are nested and their order can be different for every other file.


<Document xmlns="http://copticbook.nettrinity.co" Agpeya="true">
   <Title>
      <Language id="English">Lang 1</Language>
      <Language id="Other">Lang 2</Language>
   </Title>

   <Comment>
      <Language id="English">Lang 1</Language>
      <Language id="Other">Lang 2</Language>
   </Comment>

   <Season id="Mesh">
      <Title>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Title>

      <InsertDocument path="antiphonary/Introduction"/>

      <Section expanded="true">
         <Title>
             <Language id="English">Lang 1</Language>
             <Language id="Other">Lang 2</Language>
         </Title>
      </Section>
   </Season>

   <Reference reference="Ps" type="section"/>

   <Season id="Test">
      <Kference reference="Ac" type="section"/>
   </Season>

   <LinkDocument path="incns">
       <Language id="English">Lang 1</Language>
       <Language id="Other">Lang 2</Language>
   </LinkDocument>

   <Season id="Test">
      <LinkDocument path="Test">
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </LinkDocument>
   </Season>

   <Section expanded="true">
      <Title>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Title>

      <Text>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Text>
   </Section>

   <Section expanded="true">
      <Title>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Title>

      <Text>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Text>
   </Section>

   <Role id="Test">
      <Text>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Text>

        <Section expanded="true">
          <Title>
              <Language id="English">Lang 1</Language>
              <Language id="Other">Lang 2</Language>
          </Title>

          <Text>
              <Language id="English">Lang 1</Language>
              <Language id="Other">Lang 2</Language>
          </Text>
       </Section>

   </Role>

   <Comment>
      <Language id="English">
         The following Test are reserved
         <ul>
            <li>Test 1</li>
            <li>Test 15</li>
            <li>Test 142</li>
         </ul>
      </Language>
   </Comment>

   <Role id="Test">
     <Group>
        <Season id="Test">
           <BibleReference reference="Test" type="section"/>
        </Season>
     </Group>
  </Role>

   <Role id="Test">
      <Text>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Text>
   </Role>

   <Role id="Test">
      <Section expanded="true">
         <Title>
             <Language id="English">Lang 1</Language>
             <Language id="Other">Lang 2</Language>
         </Title>

         <Text>
             <Language id="English">Lang 1</Language>
             <Language id="Other">Lang 2</Language>
         </Text>
      </Section>
   </Role>

   <InsertDocument path="include/Test"/>

   <Section expanded="true">
      <Title>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Title>

      <Role id="Reader">
         <Text>
             <Language id="English">Lang 1</Language>
             <Language id="Other">Lang 2</Language>
         </Text>
      </Role>

      <InsertDocument path="include/Test"/>

      <Role id="Test">
         <Text>
             <Language id="English">Lang 1</Language>
             <Language id="Other">Lang 2</Language>
         </Text>
      </Role>

      <InsertDocument path="include/Test"/>

      <Role id="Test">
         <Text>
             <Language id="English">Lang 1</Language>
             <Language id="Other">Lang 2</Language>
         </Text>
      </Role>
   </Section>

   <Section expanded="true">
      <Title>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Title>

      <Text>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Text>
   </Section>

   <Section expanded="true">
      <Title>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Title>

      <Text>
          <Language id="English">Lang 1</Language>
          <Language id="Other">Lang 2</Language>
      </Text>
   </Section>

</Document>

This is incorrect implementation because the order of the nodes is still messed up. but you get the idea.

struct ArticleScene: View {
    @StateObject var viewModel: ReaderViewModel
    @State private var currentNode: XMLNode?

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                if let document = viewModel.document {
                    processNode(node: .document(document))
                }
            }
            .padding()
        }
    }

    @ViewBuilder
    private func processNode(node: XMLNode) -> some View {
        switch node {
        case .document(let document):
            if let title = document.title {
                displayTitle(title)
            }
            displayTexts(texts: document.texts)
            displaySections(sections: document.sections)
        case .text(let text):
            if let text {
                displayText(text)
            }
        case .section(let section):
            if let title = section?.title {
                displayTitle(title)
            }
            if let texts = section?.texts {
                displayTexts(texts: texts)
            }
        default:
            EmptyView()
        }
    }

    @ViewBuilder
    private func displayTitle(_ title: XML.Title) -> some View {
        displayLanguages(title.languages)
    }

    @ViewBuilder
    private func displayText(_ text: XML.Text) -> some View {
        displayLanguages(text.languages)
    }

    @ViewBuilder
    private func displaySections(sections: [XML.Section]) -> some View {
        ForEach(sections, id: \.self) { section in
            if let title = section.title {
                displayTitle(title)
            }
            displayTexts(texts: section.texts)
        }
    }

    @ViewBuilder
    private func displayTexts(texts: [XML.Text]) -> some View {
        ForEach(texts, id: \.self) { text in
            displayText(text)
        }
    }

    @ViewBuilder
    private func displayLanguages(_ languages: [XML.LanguageNode] = []) -> some View {
        ForEach(languages, id: \.self) { item in
            if let value = item.value {
                Text(value)
            }
        }
    }

}
waelsaad commented 1 year ago

Is it possible for a SWXMLHash deserialized object to have an exposed .childern property that can keep track of all the nodes in a sequential manner? In general each deserialized object should be able to know all of its nodes

    for child in document.children {
        print(child.element!.name)
    }

Thanks

waelsaad commented 1 year ago

I have implemented the .children solution on the deserialized object. I do belive this is something that should be offered by SWXMLHash. Thanks

drmohundro commented 1 year ago

Hey @waelsaad glad you were able to get it figured out! I was traveling this weekend and saw on my phone, but hadn't had a chance to reply yet. Hope everything is well!

waelsaad commented 1 year ago

Hi @drmohundro thank you very much and I appreciate the response. I hope you had a safe flight and all is well with you also. :)