Page 1 of 1
Can I format and indent specific elements after a refactoring operation?
Posted: Fri Nov 06, 2020 7:52 pm
by chrispitude
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!
Re: Can I format and indent specific elements after a refactoring operation?
Posted: Mon Nov 09, 2020 8:54 am
by Radu
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
Re: Can I format and indent specific elements after a refactoring operation?
Posted: Fri Jan 29, 2021 3:42 am
by chrispitude
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>
</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>
</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>
</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>
</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>
Re: Can I format and indent specific elements after a refactoring operation?
Posted: Sat Jan 30, 2021 4:29 pm
by chrispitude
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>
</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>
</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>
</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>
</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 -->