;; This file is part of Beastie ;; SPDX-FileCopyrightText: 2024 Norman Gray ;; 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 "" ;; 'sym2 "" ;; '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: aXY(b)") ;; (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" ;; cmd (bstring->ustring arg) cmd) ;; (sprintf "<~a/>" cmd)) ;; #f))) ;; ((*beastie* 'bstring-format-bstring) ;; (λ (us) ;; (sprintf "[~a]" us)))) ;; (assert-equal (sprintf "3a: ~a" tstr) ;; "3a: acx[e]f") ;; (assert-equal (sprintf "3s: ~s" tstr) ;; "3s: #{acx{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)))