3 SPDX-FileCopyrightText: 2026 Lady <https://www.ladys.computer/about/#lady>
4 SPDX-License-Identifier: MPL-2.0
7 ⁌ ⛩📰 书社 ∷ parsers/psj.xslt
9 © 2026 Lady [@ Ladys Computer].
11 This Source Code Form is subject to the terms of the Mozilla Public License, v 2.0.
12 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/>.
15 <!ENTITY xhtml 'http://www.w3.org/1999/xhtml'>
18 This is an alterative to the T·S·V format which allows rows to take up
19 multiple lines and separates columns with backslash‐escapable `|`s.
21 Roughly speaking, the syntax is as follows :—
23 - The file should begin with `%?psj`.
25 - Rows are terminated by lines which begin `%%`.
26 Any extra content on the line following the `%%` is treated as a
28 The final row must be terminated by `%%`, just like any other row.
30 - Rows which consist only of whitespace (including newlines) are
33 - Trailing whitespace in row lines is removed.
35 - If (after trimming) a line in a row ends in an odd number of
36 backslashes, the final one is dropped.
37 This enables a row line to end in a space by following the space
40 - Newlines in the row are then removed.
42 - Rows are broken into columns via `|` characters.
43 These can be escaped with backslashes.
44 Backslashes can also escape themselves.
46 - Whitespace is trimmed and collapsed in each column.
48 - Empty columns at the end of each row are removed.
49 Then, each row is expanded with empty columns to the width of the
52 - The first row is treated as a header and remaining rows as the body.
55 xmlns="http://www.w3.org/1999/XSL/Transform"
56 xmlns:exsl="http://exslt.org/common"
57 xmlns:exslmath="http://exslt.org/math"
58 xmlns:exslset="http://exslt.org/sets"
59 xmlns:exslstr="http://exslt.org/strings"
61 xmlns:书社="urn:fdc:ladys.computer:20231231:Shu1She4"
62 extension-element-prefixes="exsl exslmath exslset exslstr"
65 <import href="../lib/split.xslt"/>
66 <书社:id>urn:fdc:ladys.computer:20231231:Shu1She4:psj.xslt</书社:id>
67 <template name="书社:psj-fill-empty-cols">
68 <param name="header-row" select="/.."/>
69 <param name="cols-to-fill" select="0"/>
70 <if test="$cols-to-fill>0">
71 <element name="td" namespace="&xhtml;">
72 <if test="$header-row">
73 <variable name="header" select="$header-row/*[count($header-row/*)-$cols-to-fill+1]"/>
75 <attribute name="data-psj-header">
76 <value-of select="$header"/>
81 <call-template name="书社:psj-fill-empty-cols">
82 <with-param name="header-row" select="$header-row"/>
83 <with-param name="cols-to-fill" select="($cols-to-fill)-1"/>
87 <template match="html:script[@type='text/pipe-separated-jar']">
88 <variable name="lines" select="exslstr:tokenize(., '

')[not(normalize-space(.)='')]"/>
89 <variable name="shero" select="$lines[1][starts-with(., '%?psj')]"/>
90 <element name="table" namespace="&xhtml;">
91 <if test="$shero and $shero!='%?psj'">
93 <value-of select="substring-after($shero, '%?psj')"/>
96 <variable name="firstline" select="$lines[$shero and position()=2 or not($shero) and position()=1]"/>
97 <if test="starts-with($firstline, '%%') and $firstline!='%%'">
99 <value-of select="substring-after($firstline, '%%')"/>
102 <variable name="rows-fragment">
103 <for-each select="$firstline/following-sibling::*[starts-with(., '%%')]">
104 <variable name="end" select="."/>
105 <variable name="start" select="(preceding-sibling::*[starts-with(., '%%')][1]|$shero)[last()]"/>
106 <variable name="row-lines" select="exslset:intersection(exslset:trailing($end/preceding-sibling::*, $start), $lines)"/>
107 <if test="$row-lines[normalize-space(.)!='']">
108 <variable name="row">
109 <for-each select="$row-lines">
110 <variable name="line" select="normalize-space(.)"/>
112 <when test="substring($line, string-length($line))='\'">
113 <variable name="nobackslash" select="translate($line, '\', '')"/>
114 <variable name="onlybackslash" select="exslstr:tokenize($line, $nobackslash)[last()]"/>
116 <when test="string-length($onlybackslash) mod 2=1">
117 <value-of select="substring($line, 1, string-length($line)-1)"/>
120 <value-of select="$line"/>
125 <value-of select="$line"/>
130 <variable name="cols-fragment">
131 <call-template name="书社:split">
132 <with-param name="source" select="$row"/>
133 <with-param name="separator" select="'|'"/>
136 <variable name="unescaped-cols-fragment">
137 <for-each select="exsl:node-set($cols-fragment)/node()">
138 <variable name="is-last" select="position()=last()"/>
140 <variable name="nodoubles-fragment">
141 <call-template name="书社:split">
142 <with-param name="source" select="string()"/>
143 <with-param name="separator" select="'\\'"/>
146 <for-each select="exsl:node-set($nodoubles-fragment)/node()">
148 <when test="position()!=last()">
149 <value-of select="."/>
154 <when test="not($is-last) and substring(., string-length(.))='\'">
155 <value-of select="substring(., 1, string-length(.)-1)"/>
156 <processing-instruction name="书社-psj-not-finished"/>
159 <value-of select="."/>
168 <variable name="unescaped-cols" select="exsl:node-set($unescaped-cols-fragment)/node()"/>
169 <variable name="start-cols" select="$unescaped-cols[not(preceding-sibling::*[position()=1 and processing-instruction()])]"/>
171 <for-each select="$start-cols">
172 <variable name="next" select="exslset:intersection($start-cols, following-sibling::*)[1]"/>
173 <variable name="text">
174 <value-of select="text()"/>
175 <for-each select="exslset:leading(following-sibling::*, $next)">
177 <value-of select="text()"/>
181 <value-of select="normalize-space($text)"/>
186 <if test="substring-after(., '%%')!=''">
188 <value-of select="substring-after(., '%%')"/>
193 <variable name="rows" select="exsl:node-set($rows-fragment)/node()"/>
194 <variable name="lengths-fragment">
195 <for-each select="$rows[self::*]">
197 <variable name="last-col" select="*[not(following-sibling::*[.!=''])][1]"/>
199 <when test="$last-col">
200 <value-of select="1+count($last-col/preceding-sibling::*)"/>
209 <variable name="length" select="exslmath:max(exsl:node-set($lengths-fragment)/*)"/>
210 <variable name="first-row" select="$rows[1]"/>
211 <for-each select="exslset:leading($rows, $first-row)">
212 <copy-of select="."/>
214 <if test="$first-row">
215 <element name="thead" namespace="&xhtml;">
216 <element name="tr" namespace="&xhtml;">
217 <for-each select="$first-row/*">
220 <element name="th" namespace="&xhtml;">
221 <attribute name="scope">
224 <value-of select="."/>
228 <element name="td" namespace="&xhtml;"/>
232 <call-template name="书社:psj-fill-empty-cols">
233 <with-param name="cols-to-fill" select="($length)-count($first-row/*)"/>
238 <element name="tbody" namespace="&xhtml;">
239 <for-each select="$first-row/following-sibling::node()">
241 <when test="self::*">
242 <element name="tr" namespace="&xhtml;">
243 <for-each select="*">
244 <element name="td" namespace="&xhtml;">
245 <variable name="header" select="$first-row/*[count(current()/preceding-sibling::*)+1]"/>
246 <if test="$header!=''">
247 <attribute name="data-psj-header">
248 <value-of select="$header"/>
251 <value-of select="."/>
254 <call-template name="书社:psj-fill-empty-cols">
255 <with-param name="header-row" select="$first-row"/>
256 <with-param name="cols-to-fill" select="($length)-count(*)"/>
261 <copy-of select="."/>