+<?xml version="1.0"?>
+<!--
+SPDX-FileCopyrightText: 2026 Lady <https://www.ladys.computer/about/#lady>
+SPDX-License-Identifier: MPL-2.0
+-->
+<!--
+⁌ ⛩📰 书社 ∷ parsers/psj.xslt
+
+© 2026 Lady [@ Ladys Computer].
+
+This Source Code Form is subject to the terms of the Mozilla Public License, v 2.0.
+If a copy of the M·P·L was not distributed with this file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+-->
+<!DOCTYPE transform [
+ <!ENTITY xhtml 'http://www.w3.org/1999/xhtml'>
+]>
+<!--
+This is an alterative to the T·S·V format which allows rows to take up
+ multiple lines and separates columns with backslash‐escapable `|`s.
+
+Roughly speaking, the syntax is as follows :—
+
+- The file should begin with `%?psj`.
+
+- Rows are terminated by lines which begin `%%`.
+ Any extra content on the line following the `%%` is treated as a
+ comment.
+ The final row must be terminated by `%%`, just like any other row.
+
+- Rows which consist only of whitespace (including newlines) are
+ ignored.
+
+- Trailing whitespace in row lines is removed.
+
+- If (after trimming) a line in a row ends in an odd number of
+ backslashes, the final one is dropped.
+ This enables a row line to end in a space by following the space
+ with a backslash.
+
+- Newlines in the row are then removed.
+
+- Rows are broken into columns via `|` characters.
+ These can be escaped with backslashes.
+ Backslashes can also escape themselves.
+
+- Whitespace is trimmed and collapsed in each column.
+
+- Empty columns at the end of each row are removed.
+ Then, each row is expanded with empty columns to the width of the
+ widest row.
+
+- The first row is treated as a header and remaining rows as the body.
+-->
+<transform
+ xmlns="http://www.w3.org/1999/XSL/Transform"
+ xmlns:exsl="http://exslt.org/common"
+ xmlns:exslmath="http://exslt.org/math"
+ xmlns:exslset="http://exslt.org/sets"
+ xmlns:exslstr="http://exslt.org/strings"
+ xmlns:html="&xhtml;"
+ xmlns:书社="urn:fdc:ladys.computer:20231231:Shu1She4"
+ extension-element-prefixes="exsl exslmath exslset exslstr"
+ version="1.0"
+>
+ <import href="../lib/split.xslt"/>
+ <书社:id>urn:fdc:ladys.computer:20231231:Shu1She4:psj.xslt</书社:id>
+ <template name="书社:psj-fill-empty-cols">
+ <param name="header-row" select="/.."/>
+ <param name="cols-to-fill" select="0"/>
+ <if test="$cols-to-fill>0">
+ <element name="td" namespace="&xhtml;">
+ <if test="$header-row">
+ <variable name="header" select="$header-row/*[count($header-row/*)-$cols-to-fill+1]"/>
+ <if test="$header">
+ <attribute name="data-psj-header">
+ <value-of select="$header"/>
+ </attribute>
+ </if>
+ </if>
+ </element>
+ <call-template name="书社:psj-fill-empty-cols">
+ <with-param name="header-row" select="$header-row"/>
+ <with-param name="cols-to-fill" select="($cols-to-fill)-1"/>
+ </call-template>
+ </if>
+ </template>
+ <template match="html:script[@type='text/pipe-separated-jar']">
+ <variable name="lines" select="exslstr:tokenize(., '

')[not(normalize-space(.)='')]"/>
+ <variable name="shero" select="$lines[1][starts-with(., '%?psj')]"/>
+ <element name="table" namespace="&xhtml;">
+ <if test="$shero and $shero!='%?psj'">
+ <comment>
+ <value-of select="substring-after($shero, '%?psj')"/>
+ </comment>
+ </if>
+ <variable name="firstline" select="$lines[$shero and position()=2 or not($shero) and position()=1]"/>
+ <if test="starts-with($firstline, '%%') and $firstline!='%%'">
+ <comment>
+ <value-of select="substring-after($firstline, '%%')"/>
+ </comment>
+ </if>
+ <variable name="rows-fragment">
+ <for-each select="$firstline/following-sibling::*[starts-with(., '%%')]">
+ <variable name="end" select="."/>
+ <variable name="start" select="(preceding-sibling::*[starts-with(., '%%')][1]|$shero)[last()]"/>
+ <variable name="row-lines" select="exslset:intersection(exslset:trailing($end/preceding-sibling::*, $start), $lines)"/>
+ <if test="$row-lines[normalize-space(.)!='']">
+ <variable name="row">
+ <for-each select="$row-lines">
+ <variable name="line" select="normalize-space(.)"/>
+ <choose>
+ <when test="substring($line, string-length($line))='\'">
+ <variable name="nobackslash" select="translate($line, '\', '')"/>
+ <variable name="onlybackslash" select="exslstr:tokenize($line, $nobackslash)[last()]"/>
+ <choose>
+ <when test="string-length($onlybackslash) mod 2=1">
+ <value-of select="substring($line, 1, string-length($line)-1)"/>
+ </when>
+ <otherwise>
+ <value-of select="$line"/>
+ </otherwise>
+ </choose>
+ </when>
+ <otherwise>
+ <value-of select="$line"/>
+ </otherwise>
+ </choose>
+ </for-each>
+ </variable>
+ <variable name="cols-fragment">
+ <call-template name="书社:split">
+ <with-param name="source" select="$row"/>
+ <with-param name="separator" select="'|'"/>
+ </call-template>
+ </variable>
+ <variable name="unescaped-cols-fragment">
+ <for-each select="exsl:node-set($cols-fragment)/node()">
+ <variable name="is-last" select="position()=last()"/>
+ <html:span>
+ <variable name="nodoubles-fragment">
+ <call-template name="书社:split">
+ <with-param name="source" select="string()"/>
+ <with-param name="separator" select="'\\'"/>
+ </call-template>
+ </variable>
+ <for-each select="exsl:node-set($nodoubles-fragment)/node()">
+ <choose>
+ <when test="position()!=last()">
+ <value-of select="."/>
+ <text>\</text>
+ </when>
+ <otherwise>
+ <choose>
+ <when test="not($is-last) and substring(., string-length(.))='\'">
+ <value-of select="substring(., 1, string-length(.)-1)"/>
+ <processing-instruction name="书社-psj-not-finished"/>
+ </when>
+ <otherwise>
+ <value-of select="."/>
+ </otherwise>
+ </choose>
+ </otherwise>
+ </choose>
+ </for-each>
+ </html:span>
+ </for-each>
+ </variable>
+ <variable name="unescaped-cols" select="exsl:node-set($unescaped-cols-fragment)/node()"/>
+ <variable name="start-cols" select="$unescaped-cols[not(preceding-sibling::*[position()=1 and processing-instruction()])]"/>
+ <html:tr>
+ <for-each select="$start-cols">
+ <variable name="next" select="exslset:intersection($start-cols, following-sibling::*)[1]"/>
+ <variable name="text">
+ <value-of select="text()"/>
+ <for-each select="exslset:leading(following-sibling::*, $next)">
+ <text>|</text>
+ <value-of select="text()"/>
+ </for-each>
+ </variable>
+ <html:td>
+ <value-of select="normalize-space($text)"/>
+ </html:td>
+ </for-each>
+ </html:tr>
+ </if>
+ <if test="substring-after(., '%%')!=''">
+ <comment>
+ <value-of select="substring-after(., '%%')"/>
+ </comment>
+ </if>
+ </for-each>
+ </variable>
+ <variable name="rows" select="exsl:node-set($rows-fragment)/node()"/>
+ <variable name="lengths-fragment">
+ <for-each select="$rows[self::*]">
+ <html:span>
+ <variable name="last-col" select="*[not(following-sibling::*[.!=''])][1]"/>
+ <choose>
+ <when test="$last-col">
+ <value-of select="1+count($last-col/preceding-sibling::*)"/>
+ </when>
+ <otherwise>
+ <text>0</text>
+ </otherwise>
+ </choose>
+ </html:span>
+ </for-each>
+ </variable>
+ <variable name="length" select="exslmath:max(exsl:node-set($lengths-fragment)/*)"/>
+ <variable name="first-row" select="$rows[1]"/>
+ <for-each select="exslset:leading($rows, $first-row)">
+ <copy-of select="."/>
+ </for-each>
+ <if test="$first-row">
+ <element name="thead" namespace="&xhtml;">
+ <element name="tr" namespace="&xhtml;">
+ <for-each select="$first-row/*">
+ <choose>
+ <when test=".!=''">
+ <element name="th" namespace="&xhtml;">
+ <attribute name="scope">
+ <text>col</text>
+ </attribute>
+ <value-of select="."/>
+ </element>
+ </when>
+ <otherwise>
+ <element name="td" namespace="&xhtml;"/>
+ </otherwise>
+ </choose>
+ </for-each>
+ <call-template name="书社:psj-fill-empty-cols">
+ <with-param name="cols-to-fill" select="($length)-count($first-row/*)"/>
+ </call-template>
+ </element>
+ </element>
+ </if>
+ <element name="tbody" namespace="&xhtml;">
+ <for-each select="$first-row/following-sibling::node()">
+ <choose>
+ <when test="self::*">
+ <element name="tr" namespace="&xhtml;">
+ <for-each select="*">
+ <element name="td" namespace="&xhtml;">
+ <variable name="header" select="$first-row/*[count(current()/preceding-sibling::*)+1]"/>
+ <if test="$header!=''">
+ <attribute name="data-psj-header">
+ <value-of select="$header"/>
+ </attribute>
+ </if>
+ <value-of select="."/>
+ </element>
+ </for-each>
+ <call-template name="书社:psj-fill-empty-cols">
+ <with-param name="header-row" select="$first-row"/>
+ <with-param name="cols-to-fill" select="($length)-count(*)"/>
+ </call-template>
+ </element>
+ </when>
+ <otherwise>
+ <copy-of select="."/>
+ </otherwise>
+ </choose>
+ </for-each>
+ </element>
+ </element>
+ </template>
+</transform>