sergey-tihon / Clippit

Fresh PowerTools for OpenXml
https://sergey-tihon.github.io/Clippit/
MIT License
52 stars 20 forks source link

Support multi-value XPath results #38

Closed IvanDrag0 closed 3 years ago

IvanDrag0 commented 3 years ago

I have an XML file that looks like this:

<Document>
    <Paragraph>
        <p>This is text
        <p>This is also text
        <p>And this is text
    </Paragraph>
</Document>

If I wanted to list the

nodes as separate line in a document, I could use the function:

<Repeat Select="./Document/Paragraph"/>
The text is: <./p>
<EndRepeat/>

How would I go about doing something similar in a table (if I want a repeated section in a table cell)? I've tried using the

function, adding the <./p> in the cell and adding <Table Select="./Document/Paragraph"/>, but I get an error stating that there are multiple results. If I add <./Paragraph/p> to the table cell and <Table Select="./Document"/>, the <p> sections are joined together as one big sentence with no spaces between them. If I use the <Repeat> function in the table cell, I get an error as well.

sergey-tihon commented 3 years ago

@IvanDrag0 Can you please be move specific about the functionality you are talking about or share full code snippet?

IvanDrag0 commented 3 years ago

@sergey-tihon, here's a small Powershell script that just loads the assemblies and applies the data found in an XML file to the document (based on the Open-XML-PowerTools example):

Add-Type -Path "$PSScriptRoot\DocumentFormat.OpenXml.dll"
Add-Type -Path "$PSScriptRoot\Clippit.dll"

[xml]$Data = Get-Content -Path "$PSScriptRoot\Data.xml"

[Clippit.WmlDocument]$WmlDocument = [Clippit.WmlDocument]::FromDocument("$PSScriptRoot\TemplateDocument.docx")

[ref]$templateError = $null

[Clippit.WmlDocument]$wmlAssembledDoc = [Clippit.Word.DocumentAssembler]::AssembleDocument($WmlDocument, $Data, $templateError)

$wmlAssembledDoc.SaveAs("$PSScriptRoot\AssembledDocument.docx")

I'm using the following XML data:

<?xml version="1.0" encoding="utf-8"?>
<Customer>
  <Orders>
    <Order Number="1">
      <Names>
        <Name>John Smith</Name>
        <Name>Johnny Smith</Name>
        <Name>Jon Smith</Name>
      </Names>
      <ProductDescription>Unicycle</ProductDescription>
      <Quantity>3</Quantity>
    </Order>
    <Order Number="2">
      <Names>
        <Name>Jane Smith</Name>
        <Name>Jenny Smith</Name>
        <Name>Jannifer Smith</Name>
      </Names>
      <ProductDescription>Tricycle</ProductDescription>
      <Quantity>8</Quantity>
    </Order>
  </Orders>
</Customer>

The attached Word document shows what I'm trying to do and the results.

It looks like adding any of the command tags/functions inside the <Table> function will result in an invalid Word document (which Word can't open) because the library adds the literal tags into the document.xml file inside the Word DOCX package:

code

AssembledDocument.docx

TemplateDocument.docx

sergey-tihon commented 3 years ago

I do not familiar with this part of the library, but

Here is the code that generate markup for table cell - https://github.com/sergey-tihon/Clippit/blob/e0da582d4f0149788429224f5bffeae4cffe96ff/OpenXmlPowerTools/Word/DocumentAssembler.cs#L1380-L1386

It looks like it should be possible to concatenate multiple xpath results into one string and then generate one paragraph from this string - https://github.com/sergey-tihon/Clippit/blob/e0da582d4f0149788429224f5bffeae4cffe96ff/OpenXmlPowerTools/Word/DocumentAssembler.cs#L1491-L1511

IvanDrag0 commented 3 years ago

Thank you! I'll give it a try!

IvanDrag0 commented 3 years ago

My C# is a bit rusty, but I've modified the EvaluateXPathToString function to the following:

private static string EvaluateXPathToString(XElement element, string xPath, bool optional )
{
    object xPathSelectResult;
    try
    {
        //support some cells in the table may not have an xpath expression.
        if (string.IsNullOrWhiteSpace(xPath)) return string.Empty;

        string[] xPathSplit = xPath.Split('/');

        if (element.Descendants(xPathSplit.Last()).Count() > 1)
        {
            var result = element.Descendants(xPathSplit[xPathSplit.Length - 2]).Select(hl => new
            {
                Str = String.Join(", ", hl.Elements(xPathSplit.Last()).Select(x => x.Value))
            });

            var e = new XElement(xPathSplit.Last(), result.First().Str);

            element.Descendants(xPathSplit.Last()).Remove();

            element.Element(xPathSplit[xPathSplit.Length - 2]).Add(e);

        }

        xPathSelectResult = element.XPathEvaluate(xPath);

    }
    catch (XPathException e)
    {
        throw new XPathException("XPathException: " + e.Message, e);
    }

    if (xPathSelectResult is IEnumerable enumerable and not string)
    {
        var selectedData = enumerable.Cast<XObject>();
        if (!selectedData.Any())
        {
            if (optional) return string.Empty;
            throw new XPathException($"XPath expression ({xPath}) returned no results");
        }

        switch (selectedData.First())
        {
            case XElement xElement:
                return xElement.Value;
            case XAttribute attribute:
                return attribute.Value;
        }
    }

    return xPathSelectResult.ToString();
}

This changes the XML in memory to the following, before applying it to the document:

<?xml version="1.0" encoding="utf-8"?>
<Customer>
  <Orders>
    <Order Number="1">
      <Names>
        <Name>John Smith, Johnny Smith, Jon Smith</Name>
      </Names>
      <ProductDescription>Unicycle</ProductDescription>
      <Quantity>3</Quantity>
    </Order>
    <Order Number="2">
      <Names>
        <Name>Jane Smith, Jenny Smith, Jannifer Smith</Name>
      </Names>
      <ProductDescription>Tricycle</ProductDescription>
      <Quantity>8</Quantity>
    </Order>
  </Orders>
</Customer>

The two things that should be added are:

  1. Add another "separator" input parameter to the function to allow the user to use custom joining separators.
  2. Add logic to allow the user to add a new line separator (such as "").
sergey-tihon commented 3 years ago

Just release 1.8.0-beta1 that support multi-value xpath inside table cell as described in #39.

@IvanDrag0 can you please take a look/try and confirm that it cover your use case?

IvanDrag0 commented 3 years ago

@sergey-tihon Works perfectly! Thank you!

sergey-tihon commented 3 years ago

Released in 1.8.0