94768


以下のような入力HTMLを、一連の兄弟要素をグルーピングする目的でXSLT1.0で処理したいと思っています。

[code]

    <html>
      <head>
        <meta charset="utf-8" />
      </head>
      <body>
        <div>
          <p>A</p>
          <div class="case">0</div>
          <div class="case">1</div>
          <div class="case">2</div>
          <p>B</p>
          <p>C</p>
          <div class="case">3</div>
          <div class="case">4</div>
          <p>D</p>
          <p>E</p>
          <div class="case">5</div>
          <div class="case">6</div>
        </div>
      </body>
    </html>
    
[/code]

期待する出力は以下のようなものです。

[code]

    <?xml version="1.0"?>
    <html>
      <head>
        <meta charset="utf-8"/>
      </head>
      <body>
        <div>
          <p>A</p>
          <box>
            <p>0</p>
            <p>1</p>
            <p>2</p>
          </box>
          <p>B</p>
          <p>C</p>
          <box>
            <p>3</p>
            <p>4</p>
          </box>
          <p>D</p>
          <p>E</p>
          <box>
            <p>5</p>
            <p>6</p>
          </box>
        </div>
      </body>
    </html>
    
[/code]

以下のXSLを書いてみましたが、一番下の`<xsl:template match="div[contains(@class, 'case')]"
/>`が効いてしまい、boxの中身が空になってしまいます。

[code]

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="xml" indent="yes"/>
    
      <xsl:template match="@*|node()">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="div[contains(@class, 'case')][not(preceding-sibling::*[1][contains(@class, 'case')])]">
        <box>
          <xsl:apply-templates select="." mode="inbox" />
          <xsl:apply-templates select="following-sibling::div[contains(@class, 'case')][preceding-sibling::*[not(self::div[contains(@class, 'case')])][1]/following-sibling::div[contains(@class, 'case')][1] = current()]" mode="inbox" />
        </box>
      </xsl:template>
      <xsl:template match="div[contains(@class, 'case')]" mode="inbox">
        <p><xsl:apply-templates select="node()" /></p>
      </xsl:template>
      <xsl:template match="div[contains(@class, 'case')]" />
    </xsl:stylesheet>
    
[/code]

よろしければ、アドバイスお願いします。


CC BY-SA 4.0
2023-05-05T15:16:42.847
94758
2023-05-06T09:38:32.657
2023-05-05T15:17:53.133
58177
58177
question
html xsl xslt
XMLで同じクラスを持つ一連の兄弟要素をグルーピングしたい


`xsl:for-each` で `p` 要素を順次処理していくなど。

[code]

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="xml" indent="yes"/>
    
      <xsl:template match="@*|node()">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="div">
        <div>
        <xsl:for-each select="p">
          <xsl:copy-of select="."/>
          <xsl:variable name="box" select="following-sibling::div[@class='case'][count(preceding-sibling::p[1]|current())=1]"/>
          <xsl:if test="count($box) > 0">
            <box>
              <xsl:apply-templates select="$box"/>
            </box>
          </xsl:if>
        </xsl:for-each>
        </div>
      </xsl:template>
    
      <xsl:template match="div[@class='case']">
        <p><xsl:value-of select="."/></p>
      </xsl:template>
    
    </xsl:stylesheet>
    
[/code]


CC BY-SA 4.0
2023-05-05T23:05:52.797
94760
2023-05-05T23:05:52.797
47127
94758
answer

[code]

    match="div[contains(@class, 'case')][not(preceding-sibling::*[1][contains(@class, 'case')])]"
    
[/code]

と

[code]

    match="div[contains(@class, 'case')]"
    
[/code]

とでは同じpriority 0.5なのでconflictし、その中でより後ろのほうにあるテンプレートが選ばれます。

なお、こうしたconflictについては、XSLTプロセッサが

[code]

    Ambiguous rule match for /html/body[1]/div[1]/div[1] Matches both "div[fn:contains(...)]" on line 19 ... and "div[fn:contains(...)][fn:empty(...)]" on line 10 ...
    
[/code]

のようなwarningを出してくれて、それによって気付けることが多いと思います。

conflictの解決法としては、優先したいテンプレートにより高い`@priority`をつける、たとえば

[code]

    <xsl:template match="div[contains(@class, 'case')][not(preceding-sibling::*[1][contains(@class, 'case')])]" priority="1">
    
[/code]

とする、という方法がまず考えられます。しかし、`@priority`を頻繁に使うと優先度地獄になりがちなので、あまりおすすめしません。

それより、2つのテンプレートをまとめて

[code]

    <xsl:template match="div[contains(@class, 'case')]">
      <xsl:if test="not(preceding-sibling::*[1][contains(@class, 'case')])">
        <box>
          <xsl:apply-templates select="." mode="inbox" />
          <xsl:apply-templates select="following-sibling::div[contains(@class, 'case')][preceding-sibling::*[not(self::div[contains(@class, 'case')])][1]/following-sibling::div[contains(@class, 'case')][1] = current()]" mode="inbox" />
        </box>
      </xsl:if>
    </xsl:template>
    
[/code]

という1つのテンプレートにするほうが見通しがよいのではないかと思います。


CC BY-SA 4.0
2023-05-06T09:38:32.657
94768
2023-05-06T09:38:32.657
58187
94758
answer