Move SSD tree path splitting to typed Kotlin

ober

749d6d9d9aea07e34b488efd410ab141c4341b51

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 3c77b6c..a372381 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -3877,7 +3877,7 @@
                 shouldIncludeSsdPdfChoiceName shouldIncludeSsdPdfFile
                 shouldStopSsdPdfTreeScan ssdPdfDocumentIsDirectory
                 shouldIncludeSsdPdfDocument
-                trimmedNonBlankLines
+                trimmedNonBlankLines trimmedNonBlankPathParts
                 sourceBaseName canonicalSourceName isUsefulGuessLabel isErrorStatus
                 formatActivityDuration groupDisplayLabel appendModeStatus
                 selectedGroupStatus sessionCountsStatus rotatedPageStatus
@@ -3938,6 +3938,7 @@
                 touchPointerOuterRadius touchPointerInnerRadius hitGroupPad)
         (type Int32)
         (type Float32)
+        (type Char)
         (type File)
         (type DocumentFile)
         (extern (fileIsFile (file : File)) : Bool
@@ -3950,6 +3951,10 @@
           (kotlin-member-get isFile))
         (extern (stringLines (text : String)) : (List String)
           (kotlin-member-call lines))
+        (extern (stringSplitTwoChars (text : String)
+                                     (first : Char)
+                                     (second : Char)) : (List String)
+          (kotlin-member-call split))
         (extern (androidColorRgb (red : Int32)
                                  (green : Int32)
                                  (blue : Int32)) : Int32
@@ -4076,6 +4081,18 @@
                     (begin
                       (mutable-list-add! out line)
                       out))))))
+        (def (trimmedNonBlankPathParts (text : String)
+                                       (first : Char)
+                                       (second : Char)) : (MutableList String)
+          (let ((parts (stringSplitTwoChars text first second)))
+            (for/fold ((out (mutable-list-empty String)))
+                      ((i (in-range (int32 0) (list-size parts))))
+              (let ((part (string-trim (list-ref parts i))))
+                (if (string-blank? part)
+                    out
+                    (begin
+                      (mutable-list-add! out part)
+                      out))))))
         (def (sourceBaseName (value : String)) : String
           (string-lowercase
             (string-replace-regex
@@ -8485,7 +8502,7 @@
        "    private fun writeTreeFile(parent: DocumentFile, relativeName: String, source: File) {"
        "        require(source.isFile && !Files.isSymbolicLink(source.toPath()))"
        "        require(source.length() <= MAX_ZIP_ENTRY_BYTES) { \"Tree export file exceeds byte limit\" }"
-       "        val parts = relativeName.split(File.separatorChar, '/').filter { it.isNotBlank() }"
+       "        val parts = trimmedNonBlankPathParts(relativeName, File.separatorChar, '/')"
        "        require(parts.size in 1..MAX_ZIP_DEPTH && parts.all { safePathComponent(it) })"
        "        val fileName = parts.lastOrNull() ?: return"
        "        var dir = parent"