<?xml version="1.0" encoding="utf-8"?>
<!-- assemble.xsl · In the Gesture SSG
     Gathers the monthly files in gestures/ into one chronological
     xml/gestures.xml, so that prev/next, the log, and the RSS feed all
     reason over every gesture rather than a single month's slice.

     Source document: the month list build.sh writes (.months.xml),
       <months><month href="gestures/2026-06.xml"/>…</months>

     build.sh has already validated every month against xml/gestures.rng,
     which requires each <time> to be a full xsd:dateTime with an offset
     (2026-09-02T15:57:00-04:00). This is the second gate, for what a
     grammar can't see across files:
       halts on   a month file that is not well-formed XML, or whose root
                  is not <gestures>
                  a <time> that is not a full date and time with an offset
                  (if run without the RELAX NG step)
                  two gestures whose local times share a page name
                  a work in xml/works.xml whose <date> is neither empty
                  nor a date (empty or absent means in progress)
       warns on   a gesture filed in the wrong month's file
                  a <toward> or <figure><work> naming no work in xml/works.xml
                  an image, video, or figure with no file name

     Writes (hrefs resolve against the project root, like the other
     stylesheets' result documents):
       xml/.gestures.xml.new   build.sh moves it to xml/gestures.xml
       .media.tsv              time, kind, filename: one capture per line,
                               which build.sh checks against captures/ -->
<xsl:stylesheet version="3.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:ig="https://inthegesture.art/ns"
    xmlns:err="http://www.w3.org/2005/xqt-errors"
    exclude-result-prefixes="#all"
    expand-text="yes">

  <!-- A <time> as written, if it is a full xsd:dateTime with seconds and
       an offset; otherwise (). -->
  <xsl:variable name="re-time"
      select="'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+\-]\d{2}:\d{2})$'"/>

  <xsl:function name="ig:norm-time" as="xs:string?">
    <xsl:param name="raw" as="xs:string"/>
    <xsl:variable name="t" select="normalize-space($raw)"/>
    <xsl:sequence select="$t[matches(., $re-time)][. castable as xs:dateTime]"/>
  </xsl:function>

  <!-- A gesture's page name: local wall time to the second, no offset.
       Keep in step with ig:slug in common.xsl. -->
  <xsl:function name="ig:slug" as="xs:string">
    <xsl:param name="t" as="xs:string"/>
    <xsl:sequence select="format-dateTime(xs:dateTime($t),
        '[Y0001]-[M01]-[D01]T[H01][m01][s01]')"/>
  </xsl:function>

  <xsl:template match="/months">

    <!-- ============ read, normalize, and check every month ============ -->
    <!-- One map per gesture: g = the element, t = its canonical time,
         f = the month file it came from (for messages) -->
    <xsl:variable name="entries" as="map(*)*">
      <xsl:for-each select="month">
        <xsl:variable name="href" select="string(@href)"/>
        <!-- a month file that isn't well-formed halts here, named -->
        <xsl:variable name="d" as="document-node()?">
          <xsl:try select="doc(resolve-uri($href, base-uri(.)))">
            <xsl:catch>
              <xsl:message terminate="yes">FATAL: {$href} is not well-formed XML: {normalize-space($err:description)}</xsl:message>
            </xsl:catch>
          </xsl:try>
        </xsl:variable>
        <xsl:if test="not($d/gestures)">
          <xsl:message terminate="yes">FATAL: {$href}: the root element must be gestures</xsl:message>
        </xsl:if>
        <xsl:variable name="month" select="replace($href, '^.*/|\.xml$', '')"/>
        <xsl:for-each select="$d/gestures/gesture">
          <xsl:variable name="raw" select="string((time)[1])"/>
          <xsl:variable name="t" select="ig:norm-time($raw)"/>
          <xsl:choose>
            <xsl:when test="empty($t)">
              <xsl:message terminate="yes">FATAL: {$href}: gesture {position()} has {
                if (normalize-space($raw) eq '') then 'no time'
                else 'an unreadable time, ''' || $raw || ''','
                } (expected YYYY-MM-DDTHH:MM:SS with an offset, e.g. 2026-09-02T15:57:00-04:00)</xsl:message>
            </xsl:when>
            <xsl:otherwise>
              <xsl:if test="substring($t, 1, 7) ne $month">
                <xsl:message>WARNING: {$href}: gesture {$t} is filed under {$month}; it builds normally, but belongs in {substring($t, 1, 7)}.xml</xsl:message>
              </xsl:if>
              <xsl:sequence select="map{'g': ., 't': $t, 'f': $href}"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each>
      </xsl:for-each>
    </xsl:variable>

    <!-- A gesture's local time names its page, so two gestures with the
         same local time to the second would overwrite each other, even
         if their offsets differ. Report every collision, then halt. -->
    <xsl:variable name="collisions" as="xs:string*">
      <xsl:for-each-group select="$entries" group-by="ig:slug(?t)">
        <xsl:if test="count(current-group()) gt 1">
          <xsl:message>ERROR: {count(current-group())} gestures would share the page g/{current-grouping-key()}.html: {string-join(current-group() ! (?t || ' in ' || ?f), '; ')}</xsl:message>
          <xsl:sequence select="current-grouping-key()"/>
        </xsl:if>
      </xsl:for-each-group>
    </xsl:variable>
    <xsl:if test="exists($collisions)">
      <xsl:message terminate="yes">FATAL: each gesture needs a unique local time, because the time names its page; adjust the seconds to tell them apart</xsl:message>
    </xsl:if>

    <!-- Every <toward> and <figure><work> should name a work in works.xml.
         Warn rather than halt, so a gesture can point toward a work
         before its entry is written. -->
    <xsl:variable name="works-uri" select="resolve-uri('xml/works.xml', base-uri(/))"/>
    <xsl:if test="doc-available($works-uri)">
      <xsl:variable name="ids" select="doc($works-uri)/works/work/id/normalize-space()"/>
      <xsl:for-each select="$entries">
        <xsl:variable name="e" select="."/>
        <xsl:for-each select="?g/(toward | figure/work)/normalize-space()[not(. = $ids)]">
          <xsl:message>WARNING: {$e?f}: gesture {$e?t} points toward '{.}', which has no entry in xml/works.xml</xsl:message>
        </xsl:for-each>
      </xsl:for-each>

      <!-- A work's <date> is the date it was finished: YYYY-MM-DD, or
           empty (or absent) while the work is in progress. Report every
           unreadable one, then halt, before works.xsl trips over it. -->
      <xsl:variable name="bad-dates"
          select="doc($works-uri)/works/work[date[normalize-space()]
                    [not(normalize-space() castable as xs:date)]]"/>
      <xsl:for-each select="$bad-dates">
        <xsl:message>ERROR: xml/works.xml: work '{normalize-space(id)}' has the date '{normalize-space(date)}', which isn't a date (YYYY-MM-DD)</xsl:message>
      </xsl:for-each>
      <xsl:if test="exists($bad-dates)">
        <xsl:message terminate="yes">FATAL: a work's date is the day it was finished; while a work is in progress, leave its date element empty or leave it out</xsl:message>
      </xsl:if>
    </xsl:if>

    <!-- ============ the merged, chronological gestures.xml ============ -->
    <xsl:result-document href="xml/.gestures.xml.new" method="xml" indent="no" encoding="utf-8">
      <xsl:text>&#10;</xsl:text>
      <xsl:comment> Generated by build.sh from gestures/*.xml. Edit the month files, not this one. </xsl:comment>
      <xsl:text>&#10;</xsl:text>
      <gestures>
        <xsl:for-each select="$entries">
          <xsl:sort select="xs:dateTime(?t)"/>
          <xsl:text>&#10;  </xsl:text>
          <xsl:apply-templates select="?g" mode="copy">
            <xsl:with-param name="t" select="?t" tunnel="yes"/>
          </xsl:apply-templates>
        </xsl:for-each>
        <xsl:text>&#10;</xsl:text>
      </gestures>
      <xsl:text>&#10;</xsl:text>
    </xsl:result-document>

    <!-- ============ the capture list for build.sh ============ -->
    <xsl:result-document href=".media.tsv" method="text" encoding="utf-8">
      <xsl:for-each select="$entries">
        <xsl:variable name="e" select="."/>
        <xsl:for-each select="?g/(image | video | figure)">
          <xsl:variable name="name" select="normalize-space(
              if (self::figure) then string((file/name)[1]) else string((file)[1]))"/>
          <xsl:choose>
            <xsl:when test="$name eq ''">
              <xsl:message>WARNING: {$e?f}: gesture {$e?t} has an {local-name()} entry with no file name</xsl:message>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="$e?t, local-name(), $name" separator="&#9;"/>
              <xsl:text>&#10;</xsl:text>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each>
      </xsl:for-each>
    </xsl:result-document>

    <xsl:message>Merged {count($entries)} gestures from {count(month)} month files into xml/gestures.xml</xsl:message>
  </xsl:template>

  <!-- Copy each gesture as written, with its time in canonical form -->
  <xsl:mode name="copy" on-no-match="shallow-copy"/>

  <xsl:template match="gesture/time" mode="copy">
    <xsl:param name="t" as="xs:string" tunnel="yes"/>
    <time>{$t}</time>
  </xsl:template>

</xsl:stylesheet>
