;; This file is part of Beastie <https://purl.org/nxg/dist/beastie>
;; SPDX-FileCopyrightText: 2024 Norman Gray <https://nxg.me.uk>
;; SPDX-License-Identifier: BSD-2-Clause

(module "s7unit.scm" 'unicode)

;; Note that most of the functions in parse-subtex.scm are no longer
;; exposed, and the only function now exposed here is parse-subtex.
;; Lots of this could therefore naturally be refactored and that this
;; test-suite could or should be simplified, and it is testable only
;; by using module/expose.
(module 'subtex*)

(print-warning 'push #f)

;; We do want to use the #{...} reader (only in this test module).
(unless (assv #\{ *#readers*)
  (set! *#readers* (cons (cons #\{ bstring-reader) *#readers*)))

;; For convenience, and ease of reading, we test bstring parses by
;; converting them to ustrings, and thence to ordinary strings,
;; writing for display in both cases.
;;
;; It's possible this would miss some things, though the tests are
;; generally written so that if the string->bstring doesn't produce the
;; expected result, it's pretty obvious.
;;
;; An alternative would be:
;;
;;  (assert-equal (string->bstring "a\\'eb\\'   ec\\'{e}d\\'   {e}e")
;;                #{aébécédée})
;;
;; but (a) that's potentially just testing string->bstring two ways, in
;; a way that could get confusing if it goes wrong, and (b) allows us
;; to do some implicit test of bstring->ustring at the same time.
(define* (subtex->test str (on-error #f))
  (let ((tstr (string->bstring str on-error)))
    (ustring->string
     (bstring->ustring tstr :write 'display)
     #f))) ;#f means print for display, not write

(test-suite
 "predicates"
 (let ((ta (string->bstring "a"))
       (tb (string->bstring "b")))
   (assert-true (bstring? ta))
   (assert-false (bstring? "a"))
   (assert-true (bstring=? ta (string->bstring "a")))
   (assert-false (bstring=? ta tb))
   (assert-false (bstring-empty? ta))
   (assert-true (bstring-empty? (string->bstring "")))

   ;; equivalences, various
   (assert-false (eq? ta (string->bstring "a")))
   (assert-false (eqv? ta (string->bstring "a")))
   (assert-true (equal? ta (string->bstring "a")))
   (assert-true (equivalent? ta (string->bstring "a")))))

(test-suite
 "subtex expansions"

 (assert-equal (subtex->test "a\\'eb\\'   ec\\'{e}d\\'   {e}e")
               "aébécédée")
 ;; macros with and without trailing spaces, and starting and ending the string
 (assert-equal (subtex->test "\\pounds\\pounds   \\pounds")
               "£££")
 ;; same, but with trailing strings, which shouldn't be gobbled as arguments
 (assert-equal (subtex->test "a\\pounds b\\pounds{c}\\foo{d}{e}\\pounds   ")
               "a£b£{c}\\foo{d}{e}£")
 ;; trailing \c doesn't have an argument
 (assert-equal (subtex->test "a\\c cb\\c{c}c\\c   {c}d\\c")
               "açbçcçd\\c")
 ;; unicode in the input doesn't confuse us
 (assert-equal (subtex->test "\\c c\\pounds £ç")
               "ç££ç")

 ;; Special case: when we find a single-character command expansion in
 ;; braces, the braces are swallowed.
 ;; Possibly unfortunately, this also affects cases like "{é}", which
 ;; we might want to leave unmodified.
 ;; See some discussion of this in parse-subtex.scm.
 (assert-equal (subtex->test "a{\\'e}b{é}c{\\'ee}d{e{\\'e}}")
               "aébéc{ée}d{eé}")

 ;; we effectively escape '{' and '}' by parsing them as unknown command sequences
 ;; (ie, these are not actually 'escapes' of the characters
 (assert-equal (subtex->test "a\\}b\\{c")
               "a\\}b\\{c")

 ;; unrecognised macros are passed through
 (assert-equal (subtex->test "a\\: b\\:{x}c\\foo    d\\foo{e}f\\foo  ")
               "a\\:b\\:{x}c\\foo d\\foo{e}f\\foo")
 ;; ...as are unrecognised arguments to recognised macros
 (assert-equal (subtex->test "a\\'xb\\'{x}c\\'  ")
               "a\\'{x}b\\'{x}c\\'")

 ;; command-sequences as arguments
 ;; known command operating on known command
 (assert-equal (subtex->test "\\'\\i")
               "í")
 (assert-equal (subtex->test "\\'{\\i}") ;ditto, in braces
               "í")
 ;; known+known, but the second isn't a recognised argument
 (assert-equal (subtex->test "\\c\\i")
               "\\c{ı}")
 (assert-equal (subtex->test "\\c{\\i}")
               "\\c{ı}")
 (assert-equal (subtex->test "{\\foo}")
               "{\\foo}")
 (assert-equal (subtex->test "\\c{cc}")
               "\\c{cc}")
 ;; known command operating on unknown command
 (assert-equal (subtex->test "\\c\\foo")
               "\\c{\\foo}")
 (assert-equal (subtex->test "\\c{\\foo}")
               "\\c{\\foo}")
 ;; ...or empty command
 (assert-equal (subtex->test "\\c{}")
               "\\c{}")
 ;; unknown command operating on known
 (assert-equal (subtex->test "\\foo\\i")
               "\\foo ı")
 (assert-equal (subtex->test "\\foo{\\i}")
               "\\foo ı")              ;"\\foo{ı}" would also be reasonable
 ;; empty input
 (assert-equal (subtex->test "") "")

 ;; Note that, above, we have included all of the cases of the result
 ;; starting with a ustring, a codepoint, and a (known and unknown) cmd.

 ;; we don't collapse when we find invalid characters,
 ;; and we do normalise them
 (assert-equal (string->list
                (subtex->test
                 (byte-vector->string #u(#x41 #xff #x42))))
               '(#\A #\xef #\xbf #\xbd #\B)) ;U+fffd, replacement character
 ;; for other UTF-8 hi-jinx, see test-misc.scm

 ;; various cases in "a{\\c{n}}{\\^{\\i}}{a}{ab}b\\c c"
 ;; also "\\foo{{x}x}
 ;; also check extra mappings which map \LaTeX -> "LATEX"
 ;; and implementations of eg \textbf{#1} -> **#1**

 ;; Expansion at different brace-levels.
 ;; Note the hoisting of the single é within the braces.
 (assert-equal (subtex->test "{a\\'eb{\\'ec{\\'{e}d}}}{\\'e}{{\\'e}}\\'e")
               "{aéb{éc{éd}}}ééé")
 ;; we discard braces only when there is a single expanded character within them
 (assert-equal (subtex->test "a{e}b{\\'e}c{\\'ed}f")
               "a{e}béc{éd}f")

 ;; various cases together
 (assert-equal (subtex->test "Ku\\v rt G\\\"{o}del and Ren{\\'e}e R{\\o}mer")
               "Kuřt Gödel and Renée Rømer")

 ;; empty braces
 (assert-equal (subtex->test "a{}b\\c{}d")
               "a{}b\\c{}d")

 ;; confirm we can parse \~n, without disrupting NBSP
 (assert-equal (subtex->test "a~\\~n")
               "a~ñ")

 ;; escaped braces
 ;; NOTE: getting the contents of a bstring this way is not
 ;; documented, and is here just so we can check what _really_ happened.
 ;; FIXME: this doesn't work yet, and $escapes is commented out in parse-subtex.scm
 #;(assert-equal (map object->string
                    ((string->bstring "a\\{b\\}c{d\\}\\{e}f") 'content))
               '(xxx))

 ;; specifically, contrasting things at different brace levels
 (assert-equal (subtex->test "B\\aa b G\\\"odel") "Båb Gödel")
 (assert-equal (subtex->test "B{\\aa}b {G{\\\"o}del}") "Båb {Gödel}")
 ;; escaped double-quote and '}'
 (assert-equal (subtex->test "F{\\o}o\\\"\\}") "Føo\\\"{\\}}")
 ;; inside maths, nothing should be expanded
 (assert-equal (subtex->test "B\\aa r$1\\\"e$") "Bår$1\\\"e$")
 ;; and we don't get confused by unmatched $...$,
 ;; and simply pass the content through unexpanded in this case
 (assert-equal (subtex->test "Bar$1\\\"e") "Bar$1\\\"e")

 ;; we mustn't convert things inside $...$ (though they must still be
 ;; balanced with respect to braces)
 (assert-equal (subtex->test "a$\\alpha e{~é}$b~é")
               "a$\\alpha e{~é}$b~é")
 ;; ...including within braces
 (assert-equal (subtex->test "a{$\\alpha e{~é}$b}~é")
               "a{$\\alpha e{~é}$b}~é")
 ;; and don't get confused about \$
 (assert-equal (subtex->test "a\\$\\'e")
               "a\\$é")
 ;; ...including $...\$...$
 (assert-equal (subtex->test "a$b\\$c$d")
               "a$b\\$c$d")

 (let ((ns (print-warning 'get-count)))
   (assert-equal (subtex->test "") "")      ;not an error
   ;; at one time, I parsed an all-space input string as "",
   ;; but I don't think there's an actual good reason for that
   ;;(assert-equal (subtex->test "     ") "")
   (assert-equal (subtex->test "  \\space          ") "   ")
   ;; confirm that the above tests have produced no extra warnings
   (assert-equal (print-warning 'get-count) ns))

 ;; all non-letters
 (assert-equal (subtex->test "\\!\\\"{}\\#\\$\\%\\&\\'{}\\(\\)\\*\\+\\,\\-\\.{}\\/\\0\\1\\2\\3\\4\\5\\6\\7\\8\\9\\:{}\\;\\<\\={}\\>\\?\\@\\[\\]\\^{}\\_\\`{}\\{\\|\\}")
               "\\!\\\"{}\\#\\$\\%\\&\\'{}\\(\\)\\*\\+\\,\\-\\.{}\\/\\0\\1\\2\\3\\4\\5\\6\\7\\8\\9\\:{}\\;\\<\\={}\\>\\?\\@\\[\\]\\^{}\\_\\`{}\\{\\|\\}")

 ;; invalid content, and the :on-error keyword argument:
 ;;
 ;; ...is absent.  The procedure as currently implemented doesn't throw any
 ;; errors on parse failures -- that is, we aim not to object to any input.
 ;; The only errors thrown are for internal coding errors, which shouldn't
 ;; be caught here.  I may remove this error-handling in future.

 ;; Trailing backslash: it's not 100% clear what is the correct thing
 ;; to do here.  I include this test, not because I think this is
 ;; necessarily right, but to be able to detect any (inadvertent)
 ;; changes to the behaviour here.
 (assert-equal (subtex->test "a\\") "a")
 ;; This is the case where the \v command is being called on the
 ;; trailing backslash: all bets are off here, but the following seems reasonable.
 (assert-equal (subtex->test "a\\v\\") "a")

 ;; various unicode/UTF-8 mischief
 (define (subtex->test/bytes bs)
   (subtex->test
    (byte-vector->string
     (apply byte-vector
            (map (λ (x)
                   (if (char? x)
                       (char->integer x)
                       x))
                 bs)))))

 (define (assert-funnies bs expected-ustring)
   (let ((str (subtex->test
               (byte-vector->string
                (apply byte-vector
                       (map (λ (x)
                              (if (char? x)
                                  (char->integer x)
                                  x))
                            bs))))))
     ;(printf "assert-funnies: bs=~s -> ~s~%" bs str)
     (assert-true (ustring=? str expected-ustring :collapse-replacements))))

 ;; The following test cases match the ICU version, because the
 ;; :collapse-replacements option doesn't work in that case.
 (assert-funnies '(#\a #xe0 #x80 #xaf #\b) ;overlong sequence
                 #"a���b")
 (assert-funnies '(#\a
                   #xed #xa0 #x80 #xed #xb0 #x80 ;decodes to surrogate pair
                   #\b)
                 "a������b")
 (assert-funnies '(#\a
                   #xf4 #x90 #x80 #x80 ;decodes to U+110000, out of range
                   #\b)
                 "a����b")
 (assert-funnies '(#\a #xff #\b)        ;non-UTF8 byte
                 "a�b")
 (assert-funnies '(#\a #xc3 #xa9 #x80 #\b) ;spurious continuation byte
                 "aé�b"))

(test-suite
 "user-tex-commands"

 (user-char-command "pounds" #"£")
 (user-char-command #"poundstring" "lb") ;key can be string or ustring
 (user-char-command "poundtstring" #{lb})
 (user-char-command "poundnum" #xa3)
 (user-char-command "poundsproc" (λ ()
                                   #"£££"))
 (user-char-command "poundsproci" (λ ()
                                    "££"))
 (user-char-command "poundsprocii" (λ ()
                                     #xa3))
 (user-char-command "poundsprociii" (λ ()
                                      (string->bstring "£££")))
 (user-char-command "emph" (λ (a)
                             (make-ustring (sprintf "**~a**" a))))
 (user-char-command "concat" (λ (a b)
                               (sprintf "[~a/~a]" a b)))
 (assert-equal (string->bstring "parse\\pounds.\\poundnum")
               #{parse£.£})
 (assert-equal (string->bstring "parse\\poundstring    \\poundtstring")
               #{parselb{lb}})
 (assert-equal (string->bstring "parse\\poundsproc/\\poundsproci/\\poundsprocii/\\poundsprociii")
               #{parse£££/££/£/{£££}})
 (assert-equal (string->bstring "emph\\emph{x}")
               #{emph**x**})
 (assert-equal (string->bstring "concat\\concat{x}y.")
               #{concat[x/y].})
 (assert-equal (string->bstring "strange\\unknown")
               #{strange\unknown})

 ;; check the right error is thrown
 (assert-exception :tag wrong-type-arg
                   :body (user-char-command 99 "bad"))
 ;; magic value
 (user-char-command 'kill-all-user-chars-for-testing #f)
)

(test-suite
 "ustring inputs"
 ;; the following should be directly equivalent the the versions with "string" inputs
 (assert-equal (subtex->test #"a\\c c") "aç")
 (assert-equal (subtex->test #"a{b}\\c d") "a{b}\\c{d}")
 ;; bstring has a special case when it includes a single character, so check two, too
 (assert-equal (subtex->test #"a{bx}\\c{cc}\\c{dx}") "a{bx}\\c{cc}\\c{dx}"))

 ;; for debugging
 (define (show-bstring-cpt x)
   (cond ((bstring? x) (cons 'bstring (bstring->ustring x)))
         ((integer? x) (if (< x #x80) (integer->char x) x))
         (else (cons '?? x))))

(test-suite
 "functions"
 (assert-equal (bstring-create #{a} #{b} #{{c}}) #{{a}{b}{{c}}})
 (assert-equal (bstring-create #x62 #"foo" #{bar}) #{bfoo{bar}})
 (assert-equal (bstring-create #"x" #"y" #x7a) #{xyz})
 (let ((t1 (string->bstring "abc"))
       (t2 (string->bstring "d{e}f")))
   (assert-equal (bstring-create t1 t2) #{{abc}{d{e}f}})
   (assert-equal (bstring-create #"1" t1) #{1{abc}})

   (assert-equal (bstring-car (string->bstring "")) #f)
   (assert-equal (bstring-car (string->bstring "{}a")) #f)
   (assert-equal (bstring-car t1) #x61)
   (assert-equal (bstring-car (bstring-create #x62 #"foo")) #x62)
                                        ;(assert-equal (bstring-car (bstring-create 'sym #"foo")) 'sym)
   (assert-equal (bstring-car (string->bstring "{a}bc")) #x61))

 (assert-equal (bstring-length (string->bstring "ab{cde}f")) 6)

 (let ((u1 #"a"))
   ;; check this string isn't changed when it's added to a bstring
   ;; (the implementation does things like ustring-append!)
   (assert-equal (bstring-create u1 #"b") #{ab})
   (assert-equal (bstring-create u1 #x62) #{ab})
   (assert-equal u1 #"a"))

 (assert-equal (bstring-append #{a} #{b} #{{c}d})
               #{ab{c}d})
 (assert-equal (bstring-append)
               #{})
 (assert-exception :tag wrong-type-arg
                   :body (bstring-append #{a} #"u"))

 ;; tokenising
 (assert-equal (bstring-tokenize #{ab c{d e}f g})
               '(#{ab} #{c{d e}f} #{g}))
 (assert-equal (bstring-tokenize #{ a })
               '(#{a}))
 (assert-equal (bstring-tokenize #{})
               '())
 (assert-equal (bstring-tokenize #{   })
               '())

 (assert-equal (bstring-tokenize #{ab c-d{e-f}-g}
                                 (λ (c)
                                   (not (= c #x2d))))
               '(#{ab c} #{d{e-f}} #{g}))
 (assert-exception :tag wrong-type-arg
                   :body (bstring-tokenize #"ustring"))

 (assert-equal (bstring-join '(#{a} #{b}) #{=})
               #{a=b})
 (assert-equal (bstring-join '() #{=})
               #{})
 (assert-exception :tag wrong-type-arg
                   :body (bstring-join '(#{a}) "-"))

 ;; not everything can be appended to a bstring
 (assert-exception (bstring-create 'sym))
 (assert-exception (bstring-create '(list)))
 ;; what about ordinary strings?
 )

(test-suite
 "#{reader}"
 (let ((ts #{hello}))
   (assert-true (bstring? ts))
   (assert-equal (map show-bstring-cpt ts)
                 '(#\h #\e #\l #\l #\o)))
 ;; the following #{...} includes a space, which exercises a code path
 ;; skipped by a string without
 (let ((ts #{hell\'o \b{c}d{e f}}))
   (assert-true (bstring? ts))
   (assert-equal (map show-bstring-cpt ts)
                 '(#\h #\e #\l #\l #xf3 #\space
                   #\\ #\b (bstring . #"c")
                   #\d
                   (bstring . #"e f")))
   (assert-equal (bstring->ustring ts)
                 #"helló \\b{c}d{e f}"))
 ;; #{ terminated by EOF
 (assert-exception (with-input-from-string "#{abc"
                     read))
 (let ((ts #{}))
   (assert-true (bstring? ts))
   (assert-true (bstring-empty? ts))))

(test-suite
 "writing"
 ;; writing for re-reading (writing for display has been extensively tested above)
 ;; I'm not 100% sure about these tests, because the intended target
 ;; may not be quite stable in my mind
 (let ((tstr (string->bstring "a\\c cb{c\\x}\\x")))
   (assert-equal (bstring->ustring tstr) ; :display is the default
                 #"açb{c\\x}\\x")
   (assert-equal (bstring->ustring tstr :write 'display)
                 #"açb{c\\x}\\x")
   (assert-equal (bstring->ustring tstr #f) ;implies :display
                 #"açb{c\\x}\\x")
   (assert-equal (bstring->ustring tstr :write 'display/braces)
                 #"{açb{c\\x}\\x}")
   (assert-equal (bstring->ustring tstr :write 'display/without-braces)
                 #"açbc\\x\\x")
   (assert-equal (bstring->ustring tstr :write #t)
                 #"#{açb{c\\x}\\x}")
   (assert-equal (bstring->ustring tstr :write 'write)
                 #"#{açb{c\\x}\\x}")
   ;; (assert-equal (bstring->ustring tstr #t) ;implies :write
   ;;               #"#{açb{c\\x}\\x}")

   ;; to string (no: bstring->string is no longer exposed
   ;; (assert-equal (bstring->string tstr :write 'display)
   ;;               "açb{c\\x}\\x")
   ;; (assert-equal (bstring->string tstr :write 'display)
   ;;               "#{açb{c\\x}\\x}")
   ;; (assert-equal (bstring->string tstr) ;display is the default
   ;;               "açb{c\\x}\\x")

   ;; (let ((tstr+ (bstring-create 'sym1 (string->bstring "a\\x\\y{b}") 'sym2 'sym3)))
   ;;   (assert-equal (bstring->ustring tstr+ :write #t)
   ;;                 #"sym1 # {a\\x\\y{b}} # sym2 # sym3")
   ;;   (assert-equal (format #f "1a: ~a" tstr+)
   ;;                 "1a: sym1a\\x\\y{b}sym2sym3")
   ;;   (assert-equal (format #f "1s: ~s" tstr+)
   ;;                 "1s: sym1 # {a\\x\\y{b}} # sym2 # sym3")
   ;;   (let-temporarily (((*beastie* 'strings)
   ;;                      (hash-table 'sym1 "<sym1>"
   ;;                                  'sym2 "<sym2>"
   ;;                                  'sym3 "<sym3>"))
   ;;                     ((*beastie* 'subtex-cmds)
   ;;                      (hash-table "x" (λ (arg)
   ;;                                        (values "X" #f))
   ;;                                  "y" (λ (arg)
   ;;                                        (values
   ;;                                         (sprintf "Y(~a)" (bstring->ustring arg))
   ;;                                         #f)))))
   ;;     (assert-equal (format #f "2a: ~a" tstr+)
   ;;                   "2a: <sym1>aXY(b)<sym2><sym3>")
   ;;     (assert-equal (format #f "2s: ~s" tstr+)
   ;;                   "2s: sym1 # {aXY(b)} # sym2 # sym3")))
   )

 ;; nbsp/tilde
 (let ((with-nbsp (string->bstring "a~b")))
   ;; default is display with nbsp -> "~"
   (assert-equal (bstring->ustring with-nbsp) #"a~b")
   (assert-equal (bstring->ustring with-nbsp :nbsp "+") #"a+b")
   ;; :nbsp is ignored with :write=#t
   (assert-equal (bstring->ustring with-nbsp :write #t :nbsp "+") #"#{a~b}"))

 ;; quotes aren't special in bstrings
 (let ((tstr (string->bstring "\"a\"")))
   (assert-equal (bstring->ustring tstr :write #t) #"#{\"a\"}")
   (assert-equal (bstring->ustring tstr :write 'display) #"\"a\""))

 ;; for bstrings containing only a single ustring, there is a fast path within display
 (let ((tstr (string->bstring "a")))
   (assert-equal (bstring->ustring tstr) #"a")
   (assert-equal (bstring->ustring tstr :write 'display/braces) #"{a}")
   (assert-equal (bstring->ustring tstr :write 'display/without-braces) #"a"))
 (let ((tstr (string->bstring "{a}")))
   ;; bstring contains a single bstring: not the fast path
   (assert-equal (bstring->ustring tstr) #"{a}"))

 ;; tests of alternative formats for cmd and bstring
 ;; (let ((tstr (string->bstring "a\\b c\\d{x}{e}f")))
 ;;   (let-temporarily (((*beastie* 'bstring-cmd-default)
 ;;                      (λ (cmd arg chars?)
 ;;                        (values (if arg
 ;;                                    (sprintf "<~a>~a</~a>"
 ;;                                             cmd (bstring->ustring arg) cmd)
 ;;                                    (sprintf "<~a/>" cmd))
 ;;                                #f)))
 ;;                     ((*beastie* 'bstring-format-bstring)
 ;;                      (λ (us)
 ;;                        (sprintf "[~a]" us))))
 ;;     (assert-equal (sprintf "3a: ~a" tstr)
 ;;                   "3a: a<b/>c<d>x</d>[e]f")
 ;;     (assert-equal (sprintf "3s: ~s" tstr)
 ;;                   "3s: #{a<b/>c<d>x</d>{e}f}"))
 ;;   (let-temporarily (((*beastie* 'bstring-format-bstring) values))
 ;;     (assert-equal (sprintf "3abis: ~a" tstr)
 ;;                   "3abis: a\\b c\\d{x}ef"))

 ;;   ;; verify that the let-temporarily has restored things
 ;;   (assert-equal (sprintf "4: ~a" tstr)
 ;;                 "4: a\\b c\\d{x}{e}f"))
 )

(test-suite
 "ustring->ustring"
 (let ((us #"a~b{c~d}e"))
   (assert-equal (ustring->ustring us) us)
   (assert-equal (ustring->ustring us :braces? #f) #"a~bc~de")
   (assert-equal (ustring->ustring us :nbsp "+") #"a+b{c+d}e")
   (assert-equal (ustring->ustring us :braces? #f :nbsp " ") #"a bc de"))
 (let ((s "a~é b {\\~n}{c}"))
   ;; also works with plain strings, and does the subtex parsing
   (assert-equal (ustring->ustring s) #"a~é b~ñ{c}")
   (assert-equal (ustring->ustring s :braces? #f :nbsp "+") #"a+é b+ñc")
   (assert-equal (ustring->ustring s :nbsp "+") #"a+é b+ñ{c}"))

 ;; this shouldn't be confused by behind-the-scenes case changing
 (let ((us #"He{Ll}O"))
   (assert-equal (ustring->ustring us) #"He{Ll}O") ;also sets cache
   (ustring-lowercase! us)              ;should discard cache
   ;; the lowercasing acts on the ustring, so doesn't  care about the {braces}
   (assert-equal (ustring->ustring us) #"he{ll}o"))

 ;; uppercase-string/bst creates a new string, so isn't at all affected by caches
 (let ((us #"He{Ll}o"))
   (module 'bst)
   (assert-equal (ustring->ustring us) #"He{Ll}o") ;also sets cache
   ;; this uppercasing avoids changing text in {braces}
   (assert-equal (ustring->ustring (uppercase-string/bst us)) #"HE{Ll}O")
   ;;no change (whether from cache or not)
   (assert-equal (ustring->ustring us) #"He{Ll}o")))

(test-suite
 "iterator"
 (assert-equal (map show-bstring-cpt (string->bstring "a \\emph{b} c{\\'e} {f}"))
               '(#\a #\space
                 #\\ #\e #\m #\p #\h
                 (bstring . #"b")
                 #\space #\c
                 #xe9 #\space
                 (bstring . #"f")))
 (assert-equal (map show-bstring-cpt (string->bstring "{\\'e}b")) ;start with escape
               '(233 #\b)))
