Skip to content

A single 490 produces duplicate bf:relation nodes under libxslt #273

Description

@edsu

The Blue Core project was noticing duplicate bf:relation nodes introduced when converting MARC to BIBFRAME. I worked with Claude on diagnosing this problem, which you can find a description of below. I can send a PR with the one line fix.


A 490 carrying a $v or $x alongside its $a converts to two identical
bf:relation nodes on the Work instead of one. The two bf:Relation nodes
have the same bf:relationship, the same bf:seriesEnumeration and an
equivalent bf:associatedResource; nothing distinguishes them.

This does not happen under Saxon. It happens under libxslt, so it reaches
anyone consuming the stylesheets through lxml (Python), Nokogiri (Ruby),
PHP's XSL extension, XML::LibXSLT (Perl) or plain xsltproc. The cause is a
libxslt bug, but the stylesheet is relying on the corner of XPath that libxslt
gets wrong, and a one-line change sidesteps it.

Reproducing

<!-- min490.xml -->
<record xmlns="http://www.loc.gov/MARC21/slim">
  <leader>00849nam a22002537  4500</leader>
  <controlfield tag="001">1</controlfield>
  <datafield tag="490" ind1="0" ind2=" ">
    <subfield code="a">Lund studies in geography ;</subfield>
    <subfield code="v">no. 32</subfield>
  </datafield>
</record>
$ xsltproc xsl/marc2bibframe2.xsl min490.xml | grep -c '<bf:Relation>'
2

$ java -cp saxon-he-12.4.jar net.sf.saxon.Transform \
      -s:min490.xml -xsl:xsl/marc2bibframe2.xsl | grep -c '<bf:Relation>'
1

$a alone gives one relation under both. Adding either $v or $x splits it
in two under libxslt. Same for 440, and for ind1="1".

Cause

ConvSpec-Process6-Series.xsl builds a result tree fragment holding a
bf:title, bf:identifiedBy and bf:seriesEnumeration per group of 490
subfields, each tagged with a groupNum, then takes the distinct group numbers
to decide how many bf:Relation nodes to emit
(ConvSpec-Process6-Series.xsl#L66):

<xsl:variable name="tGroupNums"
  select="$grouped490Info//@groupNum[not(.=preceding::bf:*/@groupNum[1])]" />

That distinct-values idiom asks for preceding::bf:*/@groupNum. libxslt
gets the preceding:: axis wrong when the node-set came from
exsl:node-set()
: it omits the fragment's first top-level element.

Reduced to a standalone stylesheet, with no marc2bibframe2 involved — three
siblings in a fragment, asking each of them for preceding::*:

<xsl:variable name="rtf">
  <a n="1"><deep/></a>
  <b n="2"/>
  <c n="3"><kid/></c>
</xsl:variable>
<xsl:variable name="frag" select="exsl:node-set($rtf)"/>
<xsl:for-each select="$frag/*">
  <xsl:value-of select="name()"/>:
  <xsl:for-each select="preceding::*"><xsl:value-of select="name()"/></xsl:for-each>
</xsl:for-each>
from b:   libxslt [deep]      Saxon [a deep]
from c:   libxslt [deep b]    Saxon [a deep b]

a is missing in both cases, whether the context node is an element or one of
its attributes. It looks like libxslt treats the fragment's first child as the
document element and excludes it as an ancestor — correct for a real document,
wrong for a fragment with several roots.

XPath 1.0 §2.2 says the preceding axis holds every node before the context
node in document order, excluding ancestors — a qualifies, so Saxon is right.
Move the same markup into a source document rather than a fragment and libxslt
agrees with Saxon, so the bug is specific to fragments.

The consequence here: the groupNum to compare against is never in the
node-set, so not(...) is always true, nothing is filtered, and every child of
the fragment gets its own bf:Relation. With $a only there is one child and
the output happens to be right. With $a $v there are two.

Suggested fix

Compare preceding-sibling::*/@groupNum from the element instead of
preceding::bf:*/@groupNum from the attribute. Every groupNum in the
fragment sits on a top-level child, so the sibling axis reaches all of them,
and the axis libxslt mishandles is not used at all.

Environment

  • marc2bibframe2 v3.1.0 (ed9abb0)
  • libxslt 1.1.43 (current release, via lxml 6.1.2) and libxslt 1.1.35 (macOS
    system xsltproc) — both affected
  • Saxon HE 12.4 — not affected
  • Found via marcxml2bf duplicating relation blue-core-lod/marc-bibframe#2, where a
    duplicate-value check on the resulting graph rejected the record

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions