Can I format and indent specific elements after a refactoring operation?

Are you missing a feature? Request it's implementation here.
chrispitude
Posts: 326
Joined: Thu May 02, 2019 2:32 pm

Can I format and indent specific elements after a refactoring operation?

Post by chrispitude » Fri Nov 06, 2020 7:52 pm

This is an extension to the following feature request, but more fine-grained:

post56094.html

I have a refactoring operation that restructures MathML blocks inside DITA documents. The refactoring causes the MathML to get funny formatting/indenting, as elements are removed/added but surrounding PCDATA whitespace nodes come along for the ride:

Code: Select all

                  </mfenced><mtext>= </mtext><mfenced separators="">
...
                  </mfenced><mo>=</mo><msub>
                    <mtext mathvariant="bold">M</mtext>
                    <mi>I</mi>
                  </msub><mtext mathvariant="bold">I</mtext></mtd></mtr></mtable></mrow>
        </math>
      </mathml>
The nicely formatted MathML XMLbecomes something of a mess after restructuring.

I don't want to use <xsl:output> to force the document to be reformatted because this could introduce false Git differences in areas of the document not affected by the refactoring.

Can Oxygen provide an application-specific PI/attribute that I can set on an element to cause content to be "format/indent"ed from that level down? This special PI/attribute would only be a post-processing directive to Oxygen; it would not be included in the final output. Something like this:

Code: Select all

<xsl:template match="mathml">
  <xsl:copy>
    <?oxygen format-and-indent?>
...
  </xsl:copy>
</xsl:template>
Thanks!

Radu
Posts: 7468
Joined: Fri Jul 09, 2004 5:18 pm

Re: Can I format and indent specific elements after a refactoring operation?

Post by Radu » Mon Nov 09, 2020 8:54 am

Hi Chris,

Thanks for the improvement request, I added for it an issue "EXM-46695 XML Refactor - format and indent only the modified content".
I cannot give a timeline for fixing it though.

Regards,
Radu
Radu Coravu
<oXygen/> XML Editor
http://www.oxygenxml.com

chrispitude
Posts: 326
Joined: Thu May 02, 2019 2:32 pm

Re: Can I format and indent specific elements after a refactoring operation?

Post by chrispitude » Fri Jan 29, 2021 3:42 am

While working on an unrelated transformation, I found a hacky solution to this problem by applying multiple XSLT template passes. In pass 1, I make the structural XML changes I want. In pass 2, I discard all whitespace nodes around the elements of interest, then re-indent them according to their depth (number of parents).

This is a refactoring operation that adds an @author attribute to top-level topic element metadata:

Code: Select all

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="xs"
  version="2.0">

  <xsl:param name="this.user" as="xs:string"/>


  <!-- PASS 1 - baseline identity transform -->
  <xsl:template match="@*|node()" mode="pass1">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" mode="pass1"/>
    </xsl:copy>
  </xsl:template>

  <!-- PASS 1 - if no prolog at all, add it -->
  <xsl:template match='/*[self::topic or self::reference-section or self::glossgroup]
    [not(./prolog)]' mode="pass1">
    <xsl:copy select=".">
      <xsl:apply-templates select="@*" mode="pass1"/>
      <prolog><author><xsl:value-of select='$this.user'/></author></prolog>
      <xsl:apply-templates select="node()" mode="pass1"/>
    </xsl:copy>
  </xsl:template>

  <!-- PASS 1 - if prolog but no author, add it -->
  <xsl:template match='/*[self::topic or self::reference-section or self::glossgroup]/prolog
    [not(./author[text() = $this.user])]' mode="pass1">
    <xsl:copy select=".">
      <xsl:apply-templates select="@*" mode="pass1"/>
      <author><xsl:value-of select='$this.user'/></author>
      <xsl:apply-templates select="node()" mode="pass1"/>
    </xsl:copy>
  </xsl:template>


  <!-- PASS 2 - baseline identity transform -->
  <xsl:template match="@*|node()" mode="pass2">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" mode="pass2"/>
    </xsl:copy>
  </xsl:template>

  <!-- PASS 2 - reformat <prolog> in root element -->
  <xsl:template match="prolog[count(ancestor::*)=1]" mode="pass2">
    <xsl:text>&#xa;</xsl:text>
    <xsl:for-each select="1 to count(ancestor::*)"><xsl:text>  </xsl:text></xsl:for-each>

    <xsl:copy select=".">
      <xsl:apply-templates select="@*" mode="pass2"/>
      <xsl:apply-templates select="node()" mode="pass2"/>
    </xsl:copy>

    <xsl:text>&#xa;</xsl:text>
    <xsl:for-each select="1 to count(ancestor::*)"><xsl:text>  </xsl:text></xsl:for-each>
  </xsl:template>

  <!-- PASS 2 - reformat content inside <prolog> -->
  <xsl:template match="*[ancestor::*[self::prolog][count(ancestor::*)=1]]" mode="pass2">
    <xsl:if test="not(ancestor::*[self::indexterm])">
      <xsl:text>&#xa;</xsl:text>
      <xsl:for-each select="1 to count(ancestor::*)"><xsl:text>  </xsl:text></xsl:for-each>
    </xsl:if>

    <xsl:copy select=".">
      <xsl:apply-templates select="@*" mode="pass2"/>
      <xsl:apply-templates select="node()" mode="pass2"/>
    </xsl:copy>

    <xsl:if test="not(following-sibling::*) and not(ancestor::*[self::indexterm])">
      <xsl:text>&#xa;</xsl:text>
      <xsl:for-each select="1 to (count(ancestor::*)-1)"><xsl:text>  </xsl:text></xsl:for-each>
    </xsl:if>
  </xsl:template>

  <!-- PASS 2 - remove blank lines in/around prolog -->
  <xsl:template match="text()[following-sibling::node()[1][self::prolog][count(ancestor::*)=1]][matches(., '^\s*\n\s*$')]" mode="pass2"/>
  <xsl:template match="text()[ancestor-or-self::node()[self::prolog][count(ancestor::*)=1]][matches(., '^\s*\n\s*$')]" mode="pass2"/>
  <xsl:template match="text()[preceding-sibling::node()[1][self::prolog][count(ancestor::*)=1]][matches(., '^\s*\n\s*$')]" mode="pass2"/>


  <!-- TOP-LEVEL PASS - baseline identity transform -->
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

  <!-- TOP-LEVEL PASS - apply passes 1 and 2 to document -->
  <xsl:template match="/*[not(ends-with(@id, '_project_list'))][not(tokenize(@outputclass, '\s+') = 'common')]">
    <xsl:variable name="p1">
      <xsl:apply-templates select="." mode="pass1"/>
    </xsl:variable>
    <xsl:variable name="p2">
      <xsl:apply-templates select="$p1" mode="pass2"/>
    </xsl:variable>
    <xsl:copy-of select="$p2"/>
  </xsl:template>

</xsl:stylesheet>
The corresponding XML refactoring description for Oxygen is:

Code: Select all

<?xml version="1.0" encoding="UTF-8"?>
<refactoringOperationDescriptor
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.oxygenxml.com/ns/xmlRefactoring" id="add-author"
    name="Add author metadata">
    <description>Add author/owner information to topic metadata.</description>
    <script type="XSLT" href="add-author.xsl"/>
    <category>- SNPS DITA Author Utilities</category>
    <parameters>
        <description>Configuration options</description>
        <parameter type="TEXT" name="this.user" label="User name">
            <description>User name</description>
        </parameter>
    </parameters>
</refactoringOperationDescriptor>

chrispitude
Posts: 326
Joined: Thu May 02, 2019 2:32 pm

Re: Can I format and indent specific elements after a refactoring operation?

Post by chrispitude » Sat Jan 30, 2021 4:29 pm

I made some simplifications and bug fixes to add-author.xsl:

Code: Select all

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="xs"
  version="2.0">

  <xsl:param name="this.user" as="xs:string"/>


  <!-- PASS 1 - baseline identity transform -->
  <xsl:template match="@*|node()" mode="pass1">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" mode="pass1"/>
    </xsl:copy>
  </xsl:template>

  <!-- PASS 1 - if no prolog at all, add it -->
  <xsl:template match='/*[./body][not(./prolog)]' mode="pass1">
    <xsl:copy select=".">
      <xsl:apply-templates select="@*" mode="pass1"/>
      <xsl:apply-templates select="node()[following-sibling::body]" mode="pass1"/>                <!-- copy up to <body> -->
      <prolog>
        <xsl:attribute name="MODIFIED-PROLOG">1</xsl:attribute>
        <author><xsl:value-of select='$this.user'/></author>
      </prolog>                       <!-- add prolog with author -->
      <xsl:apply-templates select="node()[self::body or preceding-sibling::body]" mode="pass1"/>  <!-- copy <body> and beyond -->
    </xsl:copy>
  </xsl:template>

  <!-- PASS 1 - if prolog but no author, add it -->
  <xsl:template match='/*/prolog[not(./author[text() = $this.user])]' mode="pass1">
    <xsl:copy select=".">
      <xsl:apply-templates select="@*" mode="pass1"/>
      <xsl:attribute name="MODIFIED-PROLOG">1</xsl:attribute>
      <xsl:variable name="authors" as="node()+">                         <!-- get all existing <author> elements plus our new one -->
        <author><xsl:value-of select='$this.user'/></author>
        <xsl:apply-templates select="author" mode="pass1"/>
      </xsl:variable>
      <xsl:for-each select="$authors">                                   <!-- sort the sequence alphabetically -->
        <xsl:sort select="lower-case(.)"/>
        <xsl:copy-of select="."/>
      </xsl:for-each>
      <xsl:apply-templates select="node() except author" mode="pass1"/>  <!-- copy all non-<author> content -->
    </xsl:copy>
  </xsl:template>

  <!-- PASS 1 - take the opportunity to clean up some other stuff -->
  <xsl:template match='/*/prolog/metadata/keywords[not(child::*)]' mode="pass1"/>
  <xsl:template match='/*/prolog/metadata[not(./keywords[child::*])]' mode="pass1"/>


  <!-- PASS 2 - baseline identity transform -->
  <xsl:template match="@*|node()" mode="pass2">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" mode="pass2"/>
    </xsl:copy>
  </xsl:template>

  <!-- PASS 2 - remove blank lines for children before <body>, descendants in <prolog> -->
  <xsl:template match="/*/text()[following-sibling::body][matches(., '^\s*\n\s*$')]" mode="pass2"/>
  <xsl:template match="/*/prolog//text()[matches(., '^\s*\n\s*$')]" mode="pass2"/>

  <!-- PASS 2 - shallow-reformat children before <body> in root element -->
  <xsl:template match="/*/*[following-sibling::body]" mode="pass2">
    <xsl:text>&#xa;</xsl:text>
    <xsl:for-each select="1 to count(ancestor::*)"><xsl:text>  </xsl:text></xsl:for-each>

    <xsl:copy select=".">
      <xsl:apply-templates select="@*" mode="pass2"/>
      <xsl:apply-templates select="node()" mode="pass2"/>
    </xsl:copy>

  </xsl:template>

  <!-- PASS 2 - deep-reformat content inside <prolog> -->
  <xsl:template match="/*/prolog//*" mode="pass2">
    <xsl:if test="not(ancestor::*[self::indexterm])">
      <xsl:text>&#xa;</xsl:text>
      <xsl:for-each select="1 to count(ancestor::*)"><xsl:text>  </xsl:text></xsl:for-each>
    </xsl:if>

    <xsl:copy select=".">
      <xsl:apply-templates select="@*" mode="pass2"/>
      <xsl:apply-templates select="node()" mode="pass2"/>
    </xsl:copy>

    <xsl:if test="not(following-sibling::*) and not(ancestor::*[self::indexterm])">
      <xsl:text>&#xa;</xsl:text>
      <xsl:for-each select="1 to (count(ancestor::*)-1)"><xsl:text>  </xsl:text></xsl:for-each>
    </xsl:if>
  </xsl:template>

  <xsl:template match="/*/body" mode="pass2">
    <xsl:text>&#xa;</xsl:text>
    <xsl:for-each select="1 to count(ancestor::*)"><xsl:text>  </xsl:text></xsl:for-each>
    <xsl:next-match/>
  </xsl:template>

  <xsl:template match="@MODIFIED-PROLOG" mode="pass2"/>  <!-- don't need this any more -->


  <!-- TOP-LEVEL PASS - baseline identity transform -->
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

  <!-- TOP-LEVEL PASS - apply passes 1 and 2 to document -->
  <xsl:template match="/*[not(ends-with(@id, '_project_list'))][not(tokenize(@outputclass, '\s+') = 'common')]">
    <xsl:variable name="p1">
      <xsl:apply-templates select="." mode="pass1"/>
    </xsl:variable>
    <xsl:choose>
      <xsl:when test="$p1/*/prolog[@MODIFIED-PROLOG]">  <!-- only reformat the pre-<body> content if we truly changed something -->
        <xsl:variable name="p2">
          <xsl:apply-templates select="$p1" mode="pass2"/>
        </xsl:variable>
        <xsl:copy-of select="$p2"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:copy-of select="."/>  <!-- no change to <prolog> data -->
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

</xsl:stylesheet>
In this version, I wanted to apply the refactoring only if there would be a structural (non-whitespace) change. To do this, I set a temporary attribute in pass 1 if I made a structural change:

Code: Select all

      <prolog>
        <xsl:attribute name="MODIFIED-PROLOG">1</xsl:attribute>
        <author><xsl:value-of select='$this.user'/></author>
      </prolog>                       <!-- add prolog with author -->
Then in the top-level template, I run pass 2 only if the marker is present, otherwise I copy the existing content as-is with no reformatting:

Code: Select all

  <!-- TOP-LEVEL PASS - apply passes 1 and 2 to document -->
  <xsl:template match="/*[not(ends-with(@id, '_project_list'))][not(tokenize(@outputclass, '\s+') = 'common')]">
    <xsl:variable name="p1">
      <xsl:apply-templates select="." mode="pass1"/>
    </xsl:variable>
    <xsl:choose>
      <xsl:when test="$p1/*/prolog[@MODIFIED-PROLOG]">  <!-- only reformat the pre-<body> content if we truly changed something -->
        <xsl:variable name="p2">
          <xsl:apply-templates select="$p1" mode="pass2"/>
        </xsl:variable>
        <xsl:copy-of select="$p2"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:copy-of select="."/>  <!-- no change to <prolog> data -->
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
In pass 2, I have an empty template that discards my marker attribute:

Code: Select all

  <xsl:template match="@MODIFIED-PROLOG" mode="pass2"/>  <!-- don't need this any more -->

Post Reply