0

EDIT: I have corrected the code I had shared previously as there were some errors. I have tried the below suggestions Set xmldoc = CreateObject("MSXML2.DOMDocument.3.0") and Set xmldoc = CreateObject("MSXML2.DOMDocument.6.0")) with no success.


To give some context, the SVG is generated, through an Excel file I prepare, from a third party software I feed the Excel file with, so the word Item is the keyword I use to mark those paths for which I want the text to appear, this removal is to clean up the resulting SVG.


I would like to remove the string Item inside tspan, so from <tspan id="Item1-tspan" x="" y="">Item1</tspan> to <tspan id="Item1-tspan" x="" y="">1</tspan>.

I have tried all possible solutions, yet I am not able to replace text with XSLT through VBA. I would like to remove the word "Item" and I went through every single answer I found on StackOverflow and in othtr websites. I either do not get the wanted result or I get errors.

I call it with this simple macro:

VBA

Sub AddTextToSVGReplace()

Dim StrFileName As String
Dim StrFolder As String
Dim StrFolderTarget As String

Dim xmldoc As Object
Dim xsldoc As Object
Dim newdoc As Object

With Application.FileDialog(msoFileDialogFolderPicker)
    .Title = "Select the folder where the vector file is stored"
        If .Show = -1 Then
            StrFolder = .SelectedItems(1) & "\"
        End If
End With

With Application.FileDialog(msoFileDialogFolderPicker)
    .Title = "Select the folder where the edited vector file should be stored"
        If .Show = -1 Then
            StrFolderTarget = .SelectedItems(1) & "\"
        End If
End With

Set xmldoc = CreateObject("MSXML2.DOMDocument")
Set xsldoc = CreateObject("MSXML2.DOMDocument")
Set newdoc = CreateObject("MSXML2.DOMDocument")

StrFileName = Dir(StrFolder & "*.svg")

'Load XML
xmldoc.async = False
xmldoc.Load StrFileName

'Load XSL
xsldoc.async = False
xsldoc.Load StrFolder & "\" & "TextAdditionReplace.xsl"

'Transform
xmldoc.transformNodeToObject xsldoc, newdoc
newdoc.Save StrFolderTarget & "WithNames" & StrFileName


End Sub

This is the SVG file I would like to transform

SVG (extract, only relevant part)

<g id="symbols-svg">
<g id="Item1-svg" transform="translate(105, 210)">
<path d="M-5 0a5 5 0 1 0 10 0 5 5 0 1 0-10 0Z" stroke="rgb(200, 200, 200)" id="Item1" style="fill: rgb(0, 15, 60); stroke-width: 1; fill-opacity: 0.9; stroke-opacity: 0.5; stroke-linejoin: miter; stroke-linecap: butt; stroke: rgb(0, 50, 100);">
</path>
<text x="" y="" id="Item1-text" style="-inkscape-font-specification:'Calibri, Normal';font-family:Calibri;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:20px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000">
<tspan id="Item1-tspan" x="" y="">Item1</tspan>
</text>
</g>
<g id="Item2-svg" transform="translate(250, 90)">
<path d="M-5 0a5 5 0 1 0 10 0 5 5 0 1 0-10 0Z" stroke="rgb(200, 200, 200)" id="Item2" style="fill: rgb(0, 15, 60); stroke-width: 1; fill-opacity: 0.9; stroke-opacity: 0.5; stroke-linejoin: miter; stroke-linecap: butt; stroke: rgb(0, 50, 100);">
</path>
<text x="" y="" id="Item2-text" style="-inkscape-font-specification:'Calibri, Normal';font-family:Calibri;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:20px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000">
<tspan id="Item2-tspan" x="" y="">Item2</tspan>
</text>
</g>
</g>    

This is the XSLT I am using

XSLT

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    exclude-result-prefixes="svg"
    version="1.0">
<xsl:output method="xml" encoding="utf-8" omit-xml-declaration="yes" indent="yes"/>
  
  <xsl:strip-space elements="*"/>
  
  <xsl:template match="svg:g[@id[starts-with(., 'Item')]]">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
      <xsl:variable name="id" select="substring-before(@id, '-')"/>
      <text x="" y="" id="{$id}-text" style="-inkscape-font-specification:'Calibri, Normal';font-family:Calibri;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:20px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000">
        <tspan id="{$id}-tspan" x="" y="">
          <xsl:value-of select="$id"/>
        </tspan>
      </text>
    </xsl:copy>
  </xsl:template>
 
    <xsl:template name="string-replace-all">
        <xsl:param name="text" />
        <xsl:param name="replace" />
        <xsl:param name="by" />
        <xsl:choose>
            <xsl:when test="contains($text, $replace)">
                <xsl:value-of select="substring-before($text,$replace)" />
                <xsl:value-of select="$by" />
                <xsl:call-template name="string-replace-all">
                    <xsl:with-param name="text"
                        select="substring-after($text,$replace)" />
                    <xsl:with-param name="replace" select="$replace" />
                    <xsl:with-param name="by" select="$by" />
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$text" />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <!-- template call -->

    <xsl:variable name="result">
        <xsl:call-template name="string-replace-all">
            <xsl:with-param name="text" select="$text" />
            <xsl:with-param name="replace" select="'Item'" />
            <xsl:with-param name="by" select="''" />
        </xsl:call-template>
    </xsl:variable>
  
  
 <xsl:template match="processing-instruction('xml-stylesheet')"/>
 
  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Finally, this is the result I would like to have:

Wanted SVG

<g id="symbols-svg">
<g id="Item1-svg" transform="translate(105, 210)">
<path d="M-5 0a5 5 0 1 0 10 0 5 5 0 1 0-10 0Z" stroke="rgb(200, 200, 200)" id="Item1" style="fill: rgb(0, 15, 60); stroke-width: 1; fill-opacity: 0.9; stroke-opacity: 0.5; stroke-linejoin: miter; stroke-linecap: butt; stroke: rgb(0, 50, 100);">
</path>
<text x="" y="" id="Item1-text" style="-inkscape-font-specification:'Calibri, Normal';font-family:Calibri;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:20px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000">
<tspan id="Item1-tspan" x="" y="">1</tspan>
</text>
</g>
<g id="Item2-svg" transform="translate(250, 90)">
<path d="M-5 0a5 5 0 1 0 10 0 5 5 0 1 0-10 0Z" stroke="rgb(200, 200, 200)" id="Item2" style="fill: rgb(0, 15, 60); stroke-width: 1; fill-opacity: 0.9; stroke-opacity: 0.5; stroke-linejoin: miter; stroke-linecap: butt; stroke: rgb(0, 50, 100);">
</path>
<text x="" y="" id="Item2-text" style="-inkscape-font-specification:'Calibri, Normal';font-family:Calibri;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:20px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000">
<tspan id="Item2-tspan" x="" y="">2</tspan>
</text>
</g>
</g>

With <xsl:with-param name="text" select="$text" />(both CreateObject("MSXML2.DOMDocument.3.0") and "MSXML2.DOMDocument.6.0") I get error '-2147467259 (80004005)' A reference to variable or parameter 'text' cannot be resolved. The variable or parameter may not be defined, or it may not be in scope.

With <xsl:with-param name="text" select="'Item'" /> nothing happens in both cases. Nor does it with <xsl:with-param name="text" select="'{Item}'" />.

I also tried nesting as per below (it may look like blasphemy to experts)

  <xsl:template match="svg:tspan[@id[starts-with(., 'Item')]]">
    <xsl:variable name="result">
        <xsl:call-template name="string-replace-all">
            <xsl:with-param name="text" select="'Item'" />
            <xsl:with-param name="replace" select="'Item'" />
            <xsl:with-param name="by" select="''" />
        </xsl:call-template>
    </xsl:variable>
  </xsl:template>

I cannot think of any more combinations (apart of course from the correct one...).

Oran G. Utan
  • 455
  • 1
  • 2
  • 10
  • I would recommend to explicitly use `MSXML2.DomDocument.3.0` or `MSXML2.DomDocument.6.0`, for a start. If you want to use script in there, make sure you explicitly enable it. – Martin Honnen Oct 18 '22 at 23:23
  • @MartinHonnen, thank you for the reply, with MSXML2.DOMDocument60 and the library for 6.0 I get that `ActiveX component cannot create the Object` with the library for 3.0 again the Runtime error `Expression expected -->` – Oran G. Utan Oct 18 '22 at 23:42
  • Note that `id` attributes in SVG documents are not valid if they start with a digit or a hyphen, so your desired solution is not advisable. https://svgwg.org/svg2-draft/struct.html#Core.attrib but I don't really understand your motivation, though; what you are trying to achieve by shortening the `id`? Maybe replace the `item` prefix with an underscore if you really need to shorten it: https://www.w3.org/TR/xml/#NT-NameStartChar – Conal Tuohy Oct 19 '22 at 01:29
  • I was referring to all your late binding with e.g. `Set xmldoc = CreateObject("MSXML2.DOMDocument")`, there, I would recommend, to explicitly use e.g. `Set xmldoc = CreateObject("MSXML2.DOMDocument.3.0")` or `Set xmldoc = CreateObject("MSXML2.DOMDocument.6.0")`. Early binding would be e.g. `xmldoc = New MSXML2.DOMDocument60`, if my recollection serves. – Martin Honnen Oct 19 '22 at 07:49
  • @ConalTuohy, Thank you for spotting it, it was a mistake due to tiredness. I updated teh question and the code, I hope it is more clear now. – Oran G. Utan Oct 20 '22 at 18:02
  • @MartinHonnen I tried both late bindings but with no success. Early binding did not work as I got errors right away (I am still learning VBA,too) – Oran G. Utan Oct 20 '22 at 18:04
  • You raised some other XSLT questions in the context of using it from VBA. Is the problem and error running XSLT with VBA specific to this attempt to do some string replacement but you get other XSLT to run, or is it a general problem to get to use XSLT from VBA? – Martin Honnen Oct 20 '22 at 18:16
  • All other problems are solved, only this particular one is stubbornly sticking. – Oran G. Utan Oct 20 '22 at 19:01
  • I've edited my existing answer to include a stylesheet which I think should do what you want: https://stackoverflow.com/a/74120158/7372462 – Conal Tuohy Oct 21 '22 at 02:37
  • I think I have understood what is happening. My assumption was that after the first transformation, in which I add the and elements containing the keyword "Item", the following one removing the very same keyword would occur. However through the XSLT fiddle you linked I could see the removal would happen only on a file which already contained the keyword, the output of the first block is not passed to the next template, it stays for good. Here I cold find a template with proper variables named, that I will be able to study https://www.xml.com/pub/a/2002/06/05/transforming.html – Oran G. Utan Oct 22 '22 at 16:45
  • This template confirmed the above assumption, or is there a way to have a transformation that partly undoes what the preceding template block did, integrating what I had in mind? – Oran G. Utan Oct 22 '22 at 16:48

2 Answers2

2

To remove a single instance of the text Item from a string $s, you can use this expression:

concat(substring-before($s, 'Item'), substring-after($s, 'Item'))

That assumes that $s does contain the string Item (substring-before and substring-after will return an empty string if the substring is not found), but if computation takes place in a template whose match expression checks that the text Item is indeed there, then that's a safe assumption.

In your example the text Item is always at the start of the string, and your template's match checks for that case. So you could just use:

substring-after(., 'Item')

EDIT: adding a full stylesheet:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <!-- template to trim "Item" from the start of every text node -->
  <xsl:template match="text()[starts-with(., 'Item')]">
    <xsl:value-of select="substring-after(., 'Item')"/>
  </xsl:template>

  <!-- identity template copies everything else unchanged -->
  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

View as XSLT Fiddle

Conal Tuohy
  • 2,561
  • 1
  • 8
  • 15
  • Thank you, but unfortunately I would need to remove all occurrences. – Oran G. Utan Oct 20 '22 at 18:04
  • Are you actually having to deal with strings that contain more than one instance of the substring 'Item'? In your example, the strings which you are dealing with _all_ contain just one instance of 'Item', and always (if I'm not mistaken) at the start of the string – Conal Tuohy Oct 20 '22 at 23:52
  • Correct, the keyword "Item" is placed at the beginning of the string, from the previous block of the XSLT. I tried you solution, which makes perfectly sense to me, but the word stays there... – Oran G. Utan Oct 21 '22 at 11:16
  • sorry I forgot to include the "identity" template. The stylesheet works now. – Conal Tuohy Oct 21 '22 at 12:36
  • I had integrated in the existing style-sheet that included already the identity template, I even tried only the code you above, "Item" stays there. This is really a mistery to me. One of the solutions I had found was removing the term "Item", but not from the tspan. Yet I have lost the count of how many I have tried so I will have to find it again. – Oran G. Utan Oct 22 '22 at 11:59
  • 1
    The above stylesheet works for me with your sample data as input, producing SVG with the `tspan` elements containing just the text `1` and `2`, as desired. So I'm inclined to suspect there's some issue with the VBA harness rather than the XSLT. Unfortunately I'm not able to offer any help with that. I will add a link to an xslt fiddle site so you can test the XSLT outside of your VBA harness. – Conal Tuohy Oct 22 '22 at 12:38
  • I did not know something like this existed, I can try the code there without having to edit everything and run the Macro and it also says what the error is, amazing! It does say that the global variable cannot be declared `Global variable Q{}Item cannot refer to itself in its definition`, so there is indeed some missing part above where I should declare it. – Oran G. Utan Oct 22 '22 at 13:12
  • I'm puzzled because there are no global variables in my XSLT at all ... whether called `Item` or anything else. – Conal Tuohy Oct 22 '22 at 13:35
  • I mean from the code I had posted (that calls ``), I changed it to ` ` and I got that error. – Oran G. Utan Oct 22 '22 at 13:39
1

I'm adding this as another "answer" although it's not a proposed solution to your problem (unlike my other answer which is a proposed solution).

I just want to point out a couple of problems with the example code you've posted, because I think they point to a misunderstanding of how XSLT works, and I think they show you need to read up on the basics of XSLT, otherwise you're reduced to copy-and-paste of other people's code, without understanding it, in the hope that you might get lucky with some random combination.

I hope this is helpful!

This variable assignment has two problems:

<xsl:variable name="result">
    <xsl:call-template name="string-replace-all">
        <xsl:with-param name="text" select="$text" />
        <xsl:with-param name="replace" select="'Item'" />
        <xsl:with-param name="by" select="''" />
    </xsl:call-template>
</xsl:variable>

Firstly, there's an illegal reference to a variable called $text. Because the <xsl:variable name="result"> is a child of the xsl:stylesheet element, that makes it a "top-level" or "global" variable. Effectively, such variables are calculated first (and once only!) when the stylesheet is processed, and within such a variable assignment, the only variables which can be referred to are other top-level variables or stylesheet parameters. You've referred to a variable called $text but there's no top-level variable $text.

Secondly, because $result is a top-level variable, that means it's visible to code anywhere else in the stylesheet; its value can be accessed anywhere by referring to the variable name $result. But in fact there are no references to $result anywhere in the stylesheet. That means this variable assignment is "dead code"; it does nothing. Typically, a stylesheet processor would ignore this code altogether, and should probably even warn you, because "dead code" while harmless in itself is generally a sign that the programmer has made a mistake.

You also mention another attempt:

<xsl:template match="svg:tspan[@id[starts-with(., 'Item')]]">
   <xsl:variable name="result">
      <xsl:call-template name="string-replace-all">
          <xsl:with-param name="text" select="'Item'" />
          <xsl:with-param name="replace" select="'Item'" />
          <xsl:with-param name="by" select="''" />
      </xsl:call-template>
   </xsl:variable>
</xsl:template>

This template will match tspan elements whose id starts with 'Item', but what effect will the template have? It does not produce any output, so its effect will be to "eat" those tspan elements. Note that again, the $result variable is "dead code"; a variable has a value assigned but then the variable is not actually used for anything. Here, because the variable is defined within a template, the variable is visible only to following siblings (i.e. it can be referred to only within XSLT elements which follow it at the same level). But this variable assignment is the last statement in the template, so there's no way that anything can refer to it.

Conal Tuohy
  • 2,561
  • 1
  • 8
  • 15
  • Thank you for the explanation, I have been dealing with XSLT for less than two weeks (I did not even know it existed before) and I am actually starting to enjoy it. I definitely do not want to just copy/paste and develop my own stylesheets, I mostly learn by doing things. From what I understand, if you match an element without nesting such match in a block this means deleting it. Yet the insertion of the code in the other answer did not remove it. Currently, I added a part in the VBA macro to do it, but it is just brute force and all instances of "Item" are cancelled. – Oran G. Utan Oct 21 '22 at 11:21
  • That is, including those with id, hence the mistake you had spotted with them starting with an hypen `id="-tspan"` etc – Oran G. Utan Oct 21 '22 at 11:40
  • Yes if you have a template which matches a specific kind of node, the content of the template replaces the node; if the template is empty (e.g. ``) that will delete all the matching nodes. Empty templates are actually a common pattern in XSLT. – Conal Tuohy Oct 21 '22 at 12:31
  • I re-read this answer and I would say something is missing then @ https://stackoverflow.com/a/3067130/18247317, i.e. the reference to such variable. Looking around there are many versions of the same code, without any reference to how this variable is dealt with. What I struggle the most is that there is not a comprehensive guide to this online, only bits of code and one has to hope that answers have an explanation to gather some knowledge. – Oran G. Utan Oct 22 '22 at 11:57