% \iffalse meta-comment % %% File: l3text-case.dtx % % Copyright (C) 2020-2023 The LaTeX Project % % It may be distributed and/or modified under the conditions of the % LaTeX Project Public License (LPPL), either version 1.3c of this % license or (at your option) any later version. The latest version % of this license is in the file % % https://www.latex-project.org/lppl.txt % % This file is part of the "l3kernel bundle" (The Work in LPPL) % and all files in that bundle must be distributed together. % % ----------------------------------------------------------------------- % % The development version of the bundle can be found at % % https://github.com/latex3/latex3 % % for those people who are interested. % %<*driver> \documentclass[full,kernel]{l3doc} \begin{document} \DocInput{\jobname.dtx} \end{document} % % \fi % % \title{^^A % The \textsf{l3text-case} package: text processing (case changing)^^A % } % % \author{^^A % The \LaTeX{} Project\thanks % {^^A % E-mail: % \href{mailto:latex-team@latex-project.org} % {latex-team@latex-project.org}^^A % }^^A % } % % \date{Released 2023-02-01} % % \maketitle % % \begin{documentation} % % \end{documentation} % % \begin{implementation} % % \section{\pkg{l3text-case} implementation} % % \begin{macrocode} %<*package> % \end{macrocode} % % \begin{macrocode} %<@@=text> % \end{macrocode} % % \subsection{Case changing} % % \begin{variable}{\l_text_titlecase_check_letter_bool} % Needed to determine the route used in titlecasing. % \begin{macrocode} \bool_new:N \l_text_titlecase_check_letter_bool \bool_set_true:N \l_text_titlecase_check_letter_bool % \end{macrocode} % \end{variable} % % \begin{macro}[EXP] % { % \text_lowercase:n, % \text_uppercase:n, % \text_titlecase:n, % \text_titlecase_first:n % } % \begin{macro}[EXP] % { % \text_lowercase:nn, % \text_uppercase:nn, % \text_titlecase:nn, % \text_titlecase_first:nn % } % The user level functions here are all wrappers around the internal % functions for case changing. % \begin{macrocode} \cs_new:Npn \text_lowercase:n #1 { \@@_change_case:nnn { lower } { } {#1} } \cs_new:Npn \text_uppercase:n #1 { \@@_change_case:nnn { upper } { } {#1} } \cs_new:Npn \text_titlecase:n #1 { \@@_change_case:nnn { title } { } {#1} } \cs_new:Npn \text_titlecase_first:n #1 { \@@_change_case:nnn { titleonly } { } {#1} } \cs_new:Npn \text_lowercase:nn #1#2 { \@@_change_case:nnn { lower } {#1} {#2} } \cs_new:Npn \text_uppercase:nn #1#2 { \@@_change_case:nnn { upper } {#1} {#2} } \cs_new:Npn \text_titlecase:nn #1#2 { \@@_change_case:nnn { title } {#1} {#2} } \cs_new:Npn \text_titlecase_first:nn #1#2 { \@@_change_case:nnn { titleonly } {#1} {#2} } % \end{macrocode} % \end{macro} % \end{macro} % % \begin{macro}[EXP] % { % \@@_change_case:nnn , % \@@_change_case_auxi:nnn , % \@@_change_case_auxii:nnn % } % \begin{macro}[EXP]{\@@_change_case_BCP:nnn} % \begin{macro}[EXP]{\@@_change_case_BCP:nnw} % \begin{macro}[EXP]{\@@_change_case_BCP:nnnnw} % \begin{macro}[EXP] % { % \@@_change_case_store:n, \@@_change_case_store:o, % \@@_change_case_store:V, \@@_change_case_store:v, % \@@_change_case_store:e % } % \begin{macro}[EXP]{\@@_change_case_store:nw} % \begin{macro}[EXP]{\@@_change_case_result:n} % \begin{macro}[EXP]{\@@_change_case_end:w} % \begin{macro}[EXP]{\@@_change_case_loop:nnw} % \begin{macro}[EXP]{\@@_change_case_break:w} % \begin{macro}[EXP] % { % \@@_change_case_group_lower:nnn , % \@@_change_case_group_upper:nnn , % \@@_change_case_group_title:nnn , % \@@_change_case_group_titleonly:nnn % } % \begin{macro}[EXP]{\@@_change_case_space:nnw} % \begin{macro}[EXP] % {\@@_change_case_N_type:nnN, \@@_change_case_N_type_aux:nnN} % \begin{macro}[EXP]{\@@_change_case_N_type:nnnN} % \begin{macro}[EXP]{\@@_change_case_math_search:nnNNN} % \begin{macro}[EXP]{\@@_change_case_math_loop:nnNw} % \begin{macro}[EXP]{\@@_change_case_math_N_type:nnNN} % \begin{macro}[EXP]{\@@_change_case_math_group:nnNn} % \begin{macro}[EXP]{\@@_change_case_math_space:nnNw} % \begin{macro}[EXP]{\@@_change_case_cs_check:nnN} % \begin{macro}[EXP]{\@@_change_case_exclude:nnN} % \begin{macro}[EXP]{\@@_change_case_exclude:nnnN} % \begin{macro}[EXP]{\@@_change_case_exclude:nnNN} % \begin{macro}[EXP]{\@@_change_case_exclude:nnNw} % \begin{macro}[EXP]{\@@_change_case_exclude:nnNnn} % \begin{macro}[EXP]{\@@_change_case_replace:nnN} % \begin{macro}[EXP]{\@@_change_case_replace:nnn} % \begin{macro}[EXP]{\@@_change_case_switch:nnN} % \begin{macro}[EXP] % { % \@@_change_case_switch_lower:nnNnnnn , % \@@_change_case_switch_upper:nnNnnnn , % \@@_change_case_switch_title:nnNnnnn , % \@@_change_case_switch_titleonly:nnNnnnn % } % \begin{macro}[EXP] % { % \@@_change_case_letterlike_lower:nnN , % \@@_change_case_letterlike_upper:nnN , % \@@_change_case_letterlike_title:nnN , % \@@_change_case_letterlike_titleonly:nnN % } % \begin{macro}[EXP]{\@@_change_case_letterlike:nnnnN} % \begin{macro}[EXP] % { % \@@_change_case_codepoint_lower:nnn , % \@@_change_case_codepoint_upper:nnn , % \@@_change_case_codepoint_title:nnn , % \@@_change_case_codepoint_titleonly:nnn % } % \begin{macro}[EXP]{\@@_change_case_lower_sigma:nnnn} % \begin{macro}[EXP]{\@@_change_case_lower_sigma:nnnw} % \begin{macro}[EXP]{\@@_change_case_lower_sigma:nnnN} % \begin{macro}[EXP] % { % \@@_change_case_codepoint_title:nn , % \@@_change_case_codepoint_titleonly:nn % } % \begin{macro}[EXP]{\@@_change_case_codepoint_title:nnnn} % \begin{macro}[EXP] % {\@@_change_case_codepoint:nnnn, \@@_change_case_codepoint_aux:nnnn} % \begin{macro}[EXP]{\@@_change_case_codepoint:nn} % \begin{macro}[EXP] % { % \@@_change_case_codepoint:nnn , % \@@_change_case_codepoint:fnn , % \@@_change_case_codepoint_aux:nnn % } % \begin{macro}[EXP] % { % \@@_change_case_next_lower:nn , % \@@_change_case_next_upper:nn , % \@@_change_case_next_title:nn , % \@@_change_case_next_titleonly:nn , % \@@_change_case_next_end:nn % } % As for the expansion code, the business end of case changing is the % handling of \texttt{N}-type tokens. First, we expand the input fully % (so the loops here don't need to worry about awkward look-aheads and the % like). Then we split into the different paths. % % The code here needs to be \texttt{f}-type expandable to deal with the % situation where case changing is applied in running text. There, we % might have case changing as a document command and the text containing % other non-expandable document commands. % \begin{verbatim} % \cs_set_eq:NN \MakeLowercase \text_lowercase % ... % \MakeLowercase{\enquote*{A} text} % \end{verbatim} % If we use an \texttt{e}-type expansion and wrap each token in % \cs{exp_not:n}, that would explode: the document command grabs % \cs{exp_not:n} as an argument, and things go badly wrong. So we have to % wrap the entire result in exactly one \cs{exp_not:n}, or rather in the % kernel version. % \begin{macrocode} \cs_new:Npn \@@_change_case:nnn #1#2#3 { \__kernel_exp_not:w \exp_after:wN { \exp:w \exp_args:Ne \@@_change_case_auxi:nnn { \text_expand:n {#3} } {#1} {#2} } } \cs_new:Npn \@@_change_case_auxi:nnn #1#2#3 { \exp_args:No \@@_change_case_BCP:nnn { \tl_to_str:n {#3} } {#1} {#2} } \cs_new:Npx \@@_change_case_BCP:nnn #1#2#3 { \exp_not:N \@@_change_case_BCP:nnw {#2} {#3} #1 \tl_to_str:n { -x- -x- } \exp_not:N \q_@@_stop } \use:x { \cs_new:Npn \exp_not:N \@@_change_case_BCP:nnw ##1##2##3 \tl_to_str:n { -x- } ##4 \tl_to_str:n { -x- } ##5 \exp_not:N \q_@@_stop } { \@@_change_case_BCP:nnnnw {#1} {#2} {#4} {#3} #3 - - \q_@@_stop } \cs_new:Npn \@@_change_case_BCP:nnnnw #1#2#3#4#5 - #6 - #7 \q_@@_stop { \cs_if_exist:cTF { @@_change_case_ #2 _ #5 -x- #3 :nnnn } { \@@_change_case_auxii:nnn {#1} {#2} { #5 -x- #3 } } { \cs_if_exist:cTF { @@_change_case_ #2 _ #5 :nnnn } { \@@_change_case_auxii:nnn {#1} {#2} {#5} } { \@@_change_case_auxii:nnn {#1} {#2} {#4} } } } \cs_new:Npn \@@_change_case_auxii:nnn #1#2#3 { \group_align_safe_begin: \cs_if_exist_use:c { @@_change_case_boundary_ #2 _ #3 :Nnnw } \@@_change_case_loop:nnw {#2} {#3} #1 \q_@@_recursion_tail \q_@@_recursion_stop \@@_change_case_result:n { } } % \end{macrocode} % As for expansion, collect up the tokens for future use. % \begin{macrocode} \cs_new:Npn \@@_change_case_store:n #1 { \@@_change_case_store:nw {#1} } \cs_generate_variant:Nn \@@_change_case_store:n { o , e , V , v } \cs_new:Npn \@@_change_case_store:nw #1#2 \@@_change_case_result:n #3 { #2 \@@_change_case_result:n { #3 #1 } } \cs_new:Npn \@@_change_case_end:w #1 \@@_change_case_result:n #2 { \group_align_safe_end: \exp_end: #2 } % \end{macrocode} % The main loop is the standard \texttt{tl action} type. % \begin{macrocode} \cs_new:Npn \@@_change_case_loop:nnw #1#2#3 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#3} { \@@_change_case_N_type:nnN } { \tl_if_head_is_group:nTF {#3} { \use:c { @@_change_case_group_ #1 :nnn } } { \@@_change_case_space:nnw } } {#1} {#2} #3 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_break:w #1 \q_@@_recursion_tail \q_@@_recursion_stop { \@@_change_case_store:n {#1} \@@_change_case_end:w } % \end{macrocode} % For a group, we \emph{could} worry about whether this contains a character % or not. However, that would make life very complex for little gain: exactly % what a first character is is rather weakly-defined anyway. So if there is % a group, we simply assume that a character has been seen, and for title % case we switch to the \enquote{rest of the tokens} situation. To avoid % having too much testing, we use a two-step process here to allow the % titlecase functions to be separate. % \begin{macrocode} \cs_new:Npn \@@_change_case_group_lower:nnn #1#2#3 { \@@_change_case_store:o { \exp_after:wN { \exp:w \@@_change_case_auxii:nnn {#3} {#1} {#2} } } \@@_change_case_loop:nnw {#1} {#2} } \cs_new_eq:NN \@@_change_case_group_upper:nnn \@@_change_case_group_lower:nnn \cs_new:Npn \@@_change_case_group_title:nnn #1#2#3 { \@@_change_case_store:o { \exp_after:wN { \exp:w \@@_change_case_auxii:nnn {#3} {#1} {#2} } } \@@_change_case_loop:nnw { lower } {#2} } \cs_new:Npn \@@_change_case_group_titleonly:nnn #1#2#3 { \@@_change_case_store:o { \exp_after:wN { \exp:w \@@_change_case_auxii:nnn {#3} {#1} {#2} } } \@@_change_case_break:w } \use:x { \cs_new:Npn \exp_not:N \@@_change_case_space:nnw ##1##2 \c_space_tl } { \@@_change_case_store:n { ~ } \cs_if_exist_use:c { @@_change_case_boundary_ #1 _ #2 :Nnnw } \@@_change_case_loop:nnw {#1} {#2} } % \end{macrocode} % The first step of handling \texttt{N}-type tokens is to filter out the % end-of-loop. That has to be done separately from the first real step % as otherwise we pick up the wrong delimiter. The loop here is the same % as the \texttt{expand} one, just passing the additional data long. If no % close-math token is found then the final clean-up is forced % (i.e.~there is no assumption of \enquote{well-behaved} input in terms of % math mode). % \begin{macrocode} \cs_new:Npn \@@_change_case_N_type:nnN #1#2#3 { \@@_if_q_recursion_tail_stop_do:Nn #3 { \@@_change_case_end:w } \@@_change_case_N_type_aux:nnN {#1} {#2} #3 } \cs_new:Npn \@@_change_case_N_type_aux:nnN #1#2#3 { \exp_args:NV \@@_change_case_N_type:nnnN \l_text_math_delims_tl {#1} {#2} #3 } \cs_new:Npn \@@_change_case_N_type:nnnN #1#2#3#4 { \@@_change_case_math_search:nnNNN {#2} {#3} #4 #1 \q_@@_recursion_tail \q_@@_recursion_tail \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_math_search:nnNNN #1#2#3#4#5 { \@@_if_q_recursion_tail_stop_do:Nn #4 { \@@_change_case_cs_check:nnN {#1} {#2} #3 } \token_if_eq_meaning:NNTF #3 #4 { \@@_use_i_delimit_by_q_recursion_stop:nw { \@@_change_case_store:n {#3} \@@_change_case_math_loop:nnNw {#1} {#2} #5 } } { \@@_change_case_math_search:nnNNN {#1} {#2} #3 } } \cs_new:Npn \@@_change_case_math_loop:nnNw #1#2#3#4 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#4} { \@@_change_case_math_N_type:nnNN } { \tl_if_head_is_group:nTF {#4} { \@@_change_case_math_group:nnNn } { \@@_change_case_math_space:nnNw } } {#1} {#2} #3 #4 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_math_N_type:nnNN #1#2#3#4 { \@@_if_q_recursion_tail_stop_do:Nn #4 { \@@_change_case_end:w } \@@_change_case_store:n {#4} \token_if_eq_meaning:NNTF #4 #3 { \@@_change_case_loop:nnw {#1} {#2} } { \@@_change_case_math_loop:nnNw {#1} {#2} #3 } } \cs_new:Npn \@@_change_case_math_group:nnNn #1#2#3#4 { \@@_change_case_store:n { {#4} } \@@_change_case_math_loop:nnNw {#1} {#2} #3 } \use:x { \cs_new:Npn \exp_not:N \@@_change_case_math_space:nnNw ##1##2##3 \c_space_tl } { \@@_change_case_store:n { ~ } \@@_change_case_math_loop:nnNw {#1} {#2} #3 } % \end{macrocode} % Once potential math-mode cases are filtered out the next stage is to % test if the token grabbed is a control sequence: the two routes the code % may take are then very different. % \begin{macrocode} \cs_new:Npn \@@_change_case_cs_check:nnN #1#2#3 { \token_if_cs:NTF #3 { \@@_change_case_exclude:nnN {#1} {#2} } { \@@_codepoint_process:nN { \use:c { @@_change_case_codepoint_ #1 :nnn } {#1} {#2} } } #3 } % \end{macrocode} % To deal with a control sequence there is first a need to test if it is % on the list which indicate that case changing should be skipped. That's % done using a loop as for the other special cases. If a hit is found then % the argument is grabbed and passed through as-is. % \begin{macrocode} \cs_new:Npn \@@_change_case_exclude:nnN #1#2#3 { \exp_args:Ne \@@_change_case_exclude:nnnN { \exp_not:V \l_text_math_arg_tl \exp_not:V \l_text_case_exclude_arg_tl } {#1} {#2} #3 } \cs_new:Npn \@@_change_case_exclude:nnnN #1#2#3#4 { \@@_change_case_exclude:nnNN {#2} {#3} #4 #1 \q_@@_recursion_tail \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_exclude:nnNN #1#2#3#4 { \@@_if_q_recursion_tail_stop_do:Nn #4 { \@@_change_case_replace:nnN {#1} {#2} #3 } \str_if_eq:nnTF {#3} {#4} { \@@_use_i_delimit_by_q_recursion_stop:nw { \@@_change_case_exclude:nnNw {#1} {#2} #3 } } { \@@_change_case_exclude:nnNN {#1} {#2} #3 } } \cs_new:Npn \@@_change_case_exclude:nnNw #1#2#3#4# { \@@_change_case_exclude:nnNnn {#1} {#2} {#3} {#4} } \cs_new:Npn \@@_change_case_exclude:nnNnn #1#2#3#4#5 { \tl_if_blank:nTF {#4} { \@@_change_case_store:n { #3 {#5} } } { \@@_change_case_store:o { \exp_after:wN #3 \exp:w \@@_change_case_auxii:nnn {#4} {#1} {#2} {#5} } } \@@_change_case_loop:nnw {#1} {#2} } % \end{macrocode} % Deal with any specialist replacement for case changing. % \begin{macrocode} \cs_new:Npn \@@_change_case_replace:nnN #1#2#3 { \cs_if_exist:cTF { l_@@_case_ \token_to_str:N #3 _tl } { \exp_args:Nv \@@_change_case_replace:nnn { l_@@_case_ \token_to_str:N #3 _tl } {#1} {#2} } { \@@_change_case_switch:nnN {#1} {#2} #3 } } \cs_new:Npn \@@_change_case_replace:nnn #1#2#3 { \@@_change_case_loop:nnw {#2} {#3} #1 } % \end{macrocode} % Allow for manually-controlled case switching. % \begin{macrocode} \cs_new:Npn \@@_change_case_switch:nnN #1#2#3 { \cs_if_eq:NNTF #3 \text_case_switch:nnnn { \use:c { @@_change_case_switch_ #1 :nnNnnnn } } { \use:c { @@_change_case_letterlike_ #1 :nnN } } {#1} {#2} #3 } \cs_new:Npn \@@_change_case_switch_lower:nnNnnnn #1#2#3#4#5#6#7 { \@@_change_case_store:n {#6} \@@_change_case_loop:nnw {#1} {#2} } \cs_new:Npn \@@_change_case_switch_upper:nnNnnnn #1#2#3#4#5#6#7 { \@@_change_case_store:n {#5} \@@_change_case_loop:nnw {#1} {#2} } \cs_new:Npn \@@_change_case_switch_title:nnNnnnn #1#2#3#4#5#6#7 { \@@_change_case_store:n {#7} \@@_change_case_loop:nnw {#1} {#2} } \cs_new:Npn \@@_change_case_switch_titleonly:nnNnnnn #1#2#3#4#5#6#7 { \@@_change_case_store:n {#7} \@@_change_case_break:w } % \end{macrocode} % Letter-like commands may still be present: they are set up using a simple % lookup approach, so can easily be handled with no loop. If there is no % hit, we are at the end of the process: we loop around. Letter-like chars % are all available only in upper- and lowercase, so titlecasing maps to the % uppercase version. % \begin{macrocode} \cs_new:Npn \@@_change_case_letterlike_lower:nnN #1#2#3 { \@@_change_case_letterlike:nnnnN {#1} {#1} {#1} {#2} #3 } \cs_new_eq:NN \@@_change_case_letterlike_upper:nnN \@@_change_case_letterlike_lower:nnN \cs_new:Npn \@@_change_case_letterlike_title:nnN #1#2#3 { \@@_change_case_letterlike:nnnnN { upper } { lower } {#1} {#2} #3 } \cs_new:Npn \@@_change_case_letterlike_titleonly:nnN #1#2#3 { \@@_change_case_letterlike:nnnnN { upper } { end } {#1} {#2} #3 } \cs_new:Npn \@@_change_case_letterlike:nnnnN #1#2#3#4#5 { \cs_if_exist:cTF { c_@@_ #1 case_ \token_to_str:N #5 _tl } { \@@_change_case_store:v { c_@@_ #1 case_ \token_to_str:N #5 _tl } \use:c { @@_change_case_next_ #2 :nn } {#2} {#4} } { \@@_change_case_store:n {#5} \cs_if_exist:cTF { c_@@_ \str_if_eq:nnTF {#1} { lower } { upper } { lower } case_ \token_to_str:N #5 _tl } { \use:c { @@_change_case_next_ #2 :nn } {#2} {#4} } { \@@_change_case_loop:nnw {#3} {#4} } } } % \end{macrocode} % For upper- and lowercase changes, once we get to this stage there are only % a couple of questions remaining: is there a language-specific mapping and % is there the special case of a terminal sigma. If not, then we pass to % a simple codepoint mapping. % \begin{macrocode} \cs_new:Npn \@@_change_case_codepoint_lower:nnn #1#2#3 { \cs_if_exist_use:cF { @@_change_case_lower_ #2 :nnnn } { \@@_change_case_lower_sigma:nnnn } {#1} {#1} {#2} {#3} } \cs_new:Npn \@@_change_case_codepoint_upper:nnn #1#2#3 { \cs_if_exist_use:cF { @@_change_case_upper_ #2 :nnnn } { \@@_change_case_codepoint:nnnn } {#1} {#1} {#2} {#3} } % \end{macrocode} % If the current character is an uppercase sigma, the a check is made on the % next item in the input. If it is \texttt{N}-type and not a control sequence % then there is a look-ahead phase: the logic here is simply based on letters % or actives (to cover $8$-bit engines). % \begin{macrocode} \cs_new:Npn \@@_change_case_lower_sigma:nnnn #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "03A3 } { \@@_change_case_lower_sigma:nnnw {#2} } { \@@_change_case_codepoint:nnnn {#1} {#2} } {#3} {#4} } \cs_new:Npn \@@_change_case_lower_sigma:nnnw #1#2#3#4 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#4} { \@@_change_case_lower_sigma:nnnN {#3} } { \@@_change_case_store:e { \codepoint_generate:nn { "03C2 } { \@@_char_catcode:N #3 } } \@@_change_case_loop:nnw } {#1} {#2} #4 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_lower_sigma:nnnN #1#2#3#4 { \@@_change_case_store:e { \bool_lazy_or:nnTF { \token_if_letter_p:N #4 } { \bool_lazy_and_p:nn { \token_if_active_p:N #4 } { \int_compare_p:nNn {`#4} > { "80 } } } { \codepoint_generate:nn { "03C3 } { \@@_char_catcode:N #1 } } { \codepoint_generate:nn { "03C2 } { \@@_char_catcode:N #1 } } } \@@_change_case_loop:nnw {#2} {#3} #4 } % \end{macrocode} % For titlecasing, we need to fully expand the new character to see if it % is a letter (or active). % \begin{macrocode} \cs_new:Npn \@@_change_case_codepoint_title:nnn #1#2#3 { \bool_if:NTF \l_text_titlecase_check_letter_bool { \tl_if_single:nTF {#3} { \bool_lazy_or:nnTF { \token_if_letter_p:N #3 } { \bool_lazy_and_p:nn { \token_if_active_p:N #3 } { ! \int_compare_p:nNn {`#3} < { "80 } } } { \use:c { @@_change_case_codepoint_ #1 :nn } } { \@@_change_case_codepoint_title:nnnn { title } {#1} } } { \use:c { @@_change_case_codepoint_ #1 :nn } } } { \use:c { @@_change_case_codepoint_ #1 :nn } } {#2} {#3} } \cs_new_eq:NN \@@_change_case_codepoint_titleonly:nnn \@@_change_case_codepoint_title:nnn \cs_new:Npn \@@_change_case_codepoint_title:nn #1#2 { \@@_change_case_codepoint_title:nnnn { title } { lower } {#1} {#2} } \cs_new:Npn \@@_change_case_codepoint_titleonly:nn #1#2 { \@@_change_case_codepoint_title:nnnn { title } { end } {#1} {#2} } \cs_new:Npn \@@_change_case_codepoint_title:nnnn #1#2#3#4 { \cs_if_exist_use:cF { @@_change_case_title_ #3 :nnnn } { \cs_if_exist_use:cF { @@_change_case_upper_ #3 :nnnn } { \@@_change_case_codepoint:nnnn } } {#1} {#2} {#3} {#4} } \cs_new:Npn \@@_change_case_codepoint:nnnn #1#2#3#4 { \bool_lazy_and:nnTF { \tl_if_single_p:n {#4} } { \token_if_active_p:N #4 } { \@@_change_case_store:n {#4} } { \@@_change_case_store:e { \@@_change_case_codepoint:nn {#1} {#4} } } \use:c { @@_change_case_next_ #2 :nn } {#2} {#3} } \cs_new:Npn \@@_change_case_codepoint:nn #1#2 { \@@_change_case_codepoint:fnn { \int_eval:n { \@@_codepoint_from_chars:Nw #2 } } {#1} {#2} } \cs_new:Npn \@@_change_case_codepoint:nnn #1#2#3 { \exp_args:Ne \@@_change_case_codepoint_aux:nn { \__kernel_codepoint_case:nn { #2 case } {#1} } {#3} } \cs_generate_variant:Nn \@@_change_case_codepoint:nnn { f } % \end{macrocode} % Avoid high chars with p\TeX{}. % \begin{macrocode} \sys_if_engine_ptex:T { \cs_new_eq:NN \@@_change_case_codepoint_aux:nnn \@@_change_case_codepoint:nnn \cs_gset:Npn \@@_change_case_codepoint:nnn #1#2#3 { \int_compare:nNnTF {#1} = { -1 } { \exp_not:n {#3} } { \@@_change_case_codepoint_aux:nnn {#1} {#2} {#3} } } } \cs_new:Npn \@@_change_case_codepoint_aux:nn #1#2 { \use:e { \@@_change_case_codepoint_aux:nnnn #1 {#2} } } \cs_new:Npn \@@_change_case_codepoint_aux:nnnn #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = {#1} { \exp_not:n {#4} } { \codepoint_generate:nn {#1} { \@@_change_case_catcode:nn {#4} {#1} } \tl_if_blank:nF {#2} { \codepoint_generate:nn {#2} { \char_value_catcode:n {#2} } \tl_if_blank:nF {#3} { \codepoint_generate:nn {#3} { \char_value_catcode:n {#3} } } } } } % \end{macrocode} % We need to ensure that only valid catcode-extraction is attempted. That's % fine with Unicode engines but needs a bit of work with 8-bit ones. The % logic is that if the original codepoint was in the ASCII range, we keep % the catcode. Otherwise, if the target is in the ASCII range, we use % the standard catcode. If neither are true, we set as 13 on the grounds that % this will be what is used anyway! % \begin{macrocode} \bool_lazy_or:nnTF { \sys_if_engine_luatex_p: } { \sys_if_engine_xetex_p: } { \cs_new:Npn \@@_change_case_catcode:nn #1#2 { \@@_char_catcode:N #1 } } { \cs_new:Npn \@@_change_case_catcode:nn #1#2 { \@@_codepoint_compare:nNnTF {#1} < { "80 } { \@@_char_catcode:N #1 } { \int_compare:nNnTF {#2} < { "80 } { \char_value_catcode:n {#2} } { 13 } } } } \cs_new:Npn \@@_change_case_next_lower:nn #1#2 { \@@_change_case_loop:nnw {#1} {#2} } \cs_new_eq:NN \@@_change_case_next_upper:nn \@@_change_case_next_lower:nn \cs_new_eq:NN \@@_change_case_next_title:nn \@@_change_case_next_lower:nn \cs_new_eq:NN \@@_change_case_next_titleonly:nn \@@_change_case_next_lower:nn \cs_new:Npn \@@_change_case_next_end:nn #1#2 { \@@_change_case_break:w } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % % \begin{macro}{\text_declare_case_equivalent:Nn} % Create equivalents to allow replacement. % \begin{macrocode} \cs_new_protected:Npn \text_declare_case_equivalent:Nn #1#2 { \tl_clear_new:c { l_@@_case_ \token_to_str:N #1 _tl } \tl_set:cn { l_@@_case_ \token_to_str:N #1 _tl } {#2} } % \end{macrocode} % \end{macro} % % \begin{macro}{\text_case_switch:nnnn} % \begin{macro}{\@@_case_switch_marker:} % Set up the mechanism for manual case switching. % \begin{macrocode} \cs_new:Npn \text_case_switch:nnnn #1#2#3#4 { \@@_case_switch_marker: #1 } \cs_new:Npn \@@_case_switch_marker: { } % \end{macrocode} % \end{macro} % \end{macro} % % \begin{macro}[EXP] % {\@@_change_case_upper_de-x-eszett:nnnn, \@@_change_case_upper_de-alt:nnnn} % A simple alternative version for German. % \begin{macrocode} \cs_new:cpn { @@_change_case_upper_de-x-eszett:nnnn } #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "00DF } { \@@_change_case_store:e { \codepoint_generate:nn { "1E9E } { \@@_change_case_catcode:nn {#4} { "1E9E } } } \use:c { @@_change_case_next_ #2 :nn } {#2} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } \cs_new_eq:cc { @@_change_case_upper_de-alt:nnnn } { @@_change_case_upper_de-x-eszett:nnnn } % \end{macrocode} % \end{macro} % % \begin{macro}[EXP] % { % \@@_change_case_upper_el:nnnn , % \@@_change_case_upper_el_aux:nnnn , % \@@_change_case_upper_el-x-iota:nnnn % } % \begin{macro}[EXP] % {\@@_change_case_upper_el:n, \@@_change_case_upper_el-x-iota:n} % \begin{macro}[EXP]{\@@_change_case_upper_el:nnn} % \begin{macro}[EXP]{\@@_change_case_upper_el:nnnw} % \begin{macro}[EXP] % {\@@_change_case_upper_el:nnnN, \@@_change_case_upper_el_aux:nnnN} % \begin{macro}[EXP]{\@@_change_case_upper_el_ypogegrammeni:nnnnnw} % \begin{macro}[EXP]{\@@_change_case_upper_el_ypogegrammeni:nnnnnN} % \begin{macro}[EXP]{\@@_change_case_upper_el_ypogegrammeni:nnnnnn} % \begin{macro}[EXP]{\@@_change_case_upper_el_dialytika:nnn} % \begin{macro}[EXP]{\@@_change_case_upper_el_dialytika:n} % \begin{macro}[EXP]{\@@_change_case_upper_el_hiatus:nnnw} % \begin{macro}[EXP]{\@@_change_case_upper_el_hiatus:nnN} % \begin{macro}[EXP]{\@@_change_case_upper_el_hiatus:nnn} % \begin{macro}[EXP]{\@@_change_case_upper_el_gobble:nnw} % \begin{macro}[EXP]{\@@_change_case_upper_el_gobble:nnN} % \begin{macro}[EXP]{\@@_change_case_upper_el_gobble:nnn} % \begin{macro}[EXP,TF]{\@@_change_case_if_greek:n} % \begin{macro}[EXP]{\@@_change_case_if_greek:n} % \begin{macro}[EXP,pTF]{\@@_change_case_if_greek:n} % \begin{macro}[EXP,pTF]{\@@_change_case_if_greek_accent:n} % \begin{macro}[EXP]{\@@_change_case_if_greek_accent:n} % \begin{macro}[EXP,pTF]{\@@_change_case_if_greek_diacritic:n} % \begin{macro}[EXP]{\@@_change_case_if_greek_diacritic:n} % \begin{macro}[EXP,TF]{\@@_change_case_if_takes_dialytika:n} % \begin{macro}[EXP]{\@@_change_case_if_takes_dialytika:n} % \begin{macro}[EXP,TF]{\@@_change_case_if_takes_ypogegrammeni:n} % \begin{macro}[EXP]{\@@_change_case_if_takes_ypogegrammeni:n} % For Greek uppercasing, we need to know if characters \emph{in the Greek % range} have accents. That means doing a \textsc{nfd} conversion first, then % starting a search. As described by the Unicode \textsc{cldr}, Greek accents % need to be found \emph{after} any U+0308 (diaeresis) and are done in two % groups to allow for the canonical ordering. The implementation here follows % the data and examples from \textsc{icu} % (\url{https://icu.unicode.org/design/case/greek-upper}), % although necessarily the implementation is somewhat different. The % \emph{ypogegrammeni} is filtered out here as it is not actually in the % Greek range, so gets lost if we leave until later. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_el:nnnn #1#2#3#4 { \@@_change_case_if_greek:nTF {#4} { \exp_args:Ne \@@_change_case_upper_el:nnn { \codepoint_to_nfd:n { \@@_codepoint_from_chars:Nw #4 } } {#2} {#3} } { \@@_codepoint_compare:nNnTF {#4} = { "0345 } { \@@_change_case_store:e { \use:c { @@_change_case_upper_ #3 :n } {#4} } \@@_change_case_loop:nnw {#2} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } } \cs_new_eq:cN { @@_change_case_upper_el-x-iota:nnnn } \@@_change_case_upper_el:nnnn \cs_new:Npn \@@_change_case_upper_el:n #1 { \codepoint_generate:nn { "0399 } { \char_value_catcode:n { "0399 } } } \cs_new:cpn { @@_change_case_upper_el-x-iota:n } #1 { \exp_not:n {#1} } \cs_new:Npn \@@_change_case_upper_el:nnn #1#2#3 { \@@_codepoint_process:nN { \@@_change_case_upper_el:nnnw {#2} {#3} } #1 } % \end{macrocode} % At this stage we have the first NFD codepoint as |#3|. What we need to know % is whether after that we have another character, either from the NFD or % directly in the input. If not, we store the changed character at this stage. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_el:nnnw #1#2#3#4 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#4} { \@@_change_case_upper_el:nnnN {#3} } { \@@_change_case_store:e { \@@_change_case_codepoint:nn { upper } {#3} } \@@_change_case_loop:nnw } {#1} {#2} #4 \q_@@_recursion_stop } % \end{macrocode} % Now, we check the detail of the next codepoint: again we filter out the % not-a-char cases, before checking if it's an dialytika, accent or diacritic. % (The latter do not have the same hiatus behavior as accents.) There is % additional work if the codepoint can take a ypogegrammeni: there, we need % to move any ypogegrammeni to after accents (in case the input is not % normalised). The ypogegrammeni itself is handled separately. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_el:nnnN #1#2#3#4 { \token_if_cs:NTF #4 { \@@_change_case_store:e { \@@_change_case_codepoint:nn { upper } {#1} } \@@_change_case_loop:nnw {#2} {#3} #4 } { \@@_change_case_if_takes_ypogegrammeni:nTF {#1} { \@@_change_case_upper_el_ypogegrammeni:nnnnnw {#1} {#2} {#3} { } { } #4 } { \@@_change_case_upper_el_aux:nnnN {#1} {#2} {#3} #4 } } } \cs_new:Npn \@@_change_case_upper_el_ypogegrammeni:nnnnnw #1#2#3#4#5#6 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#6} { \@@_change_case_upper_el_ypogegrammeni:nnnnnN {#1} {#2} {#3} {#4} {#5} } { \@@_change_case_upper_el_aux:nnnN {#1} {#2} {#3} #4#5 } #6 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_upper_el_ypogegrammeni:nnnnnN #1#2#3#4#5#6 { \token_if_cs:NTF #6 { \@@_change_case_upper_el_aux:nnnN {#1} {#2} {#3} #4#5 #6 } { \@@_codepoint_process:nN { \@@_change_case_upper_el_ypogegrammeni:nnnnnn {#1} {#2} {#3} {#4} {#5} } #6 } } \cs_new:Npn \@@_change_case_upper_el_ypogegrammeni:nnnnnn #1#2#3#4#5#6 { \@@_codepoint_compare:nNnTF {#6} = { "0345 } { \@@_change_case_upper_el_ypogegrammeni:nnnnnw {#1} {#2} {#3} {#4} {#6} } { \bool_lazy_or:nnTF { \@@_change_case_if_greek_accent_p:n {#6} } { \@@_change_case_if_greek_diacritic_p:n {#6} } { \@@_change_case_upper_el_ypogegrammeni:nnnnnw {#1} {#2} {#3} {#4#6} {#5} } { \@@_change_case_upper_el_aux:nnnN {#1} {#2} {#3} #4#5 #6 } } } \cs_new:Npn \@@_change_case_upper_el_aux:nnnN #1#2#3#4 { \@@_codepoint_process:nN { \@@_change_case_upper_el_aux:nnnn {#1} {#2} {#3} } #4 } \cs_new:Npn \@@_change_case_upper_el_aux:nnnn #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "0308 } { \@@_change_case_upper_el_dialytika:nnn {#2} {#3} {#1} } { \@@_change_case_if_greek_accent:nTF {#4} { \@@_change_case_upper_el_hiatus:nnnw {#2} {#3} {#1} } { \@@_change_case_if_greek_diacritic:nTF {#4} { \@@_change_case_store:e { \@@_change_case_codepoint:nn { upper } {#1} } \@@_change_case_loop:nnw {#2} {#3} } { \@@_codepoint_compare:nNnTF {#4} = { "0345 } { \@@_change_case_store:e { [XXX] } } { \@@_change_case_store:e { \@@_change_case_codepoint:nn { upper } {#1} } } \@@_change_case_loop:nnw {#2} {#3} #4 } } } } % \end{macrocode} % We handle \emph{dialytika} in parts as it's also needed for the hiatus. % We know only two letters take it, so we can shortcut here on the second % part of the tests. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_el_dialytika:nnn #1#2#3 { \@@_change_case_if_takes_dialytika:nTF {#3} { \@@_change_case_upper_el_dialytika:n {#3} } { \@@_change_case_store:e { \@@_change_case_codepoint:nn { upper } {#3} } } \@@_change_case_upper_el_gobble:nnw {#1} {#2} } \cs_new:Npn \@@_change_case_upper_el_dialytika:n #1 { \@@_change_case_store:e { \bool_lazy_or:nnTF { \@@_codepoint_compare_p:nNn {#1} = { "0399 } } { \@@_codepoint_compare_p:nNn {#1} = { "03B9 } } { \codepoint_generate:nn { "03AA } { \@@_change_case_catcode:nn {#1} { "03AA } } } { \codepoint_generate:nn { "03AB } { \@@_change_case_catcode:nn {#1} { "03AB } } } } } % \end{macrocode} % Adding a hiatus needs some of the same ideas, but if there is not one we % skip this code point, hence needing a separate function. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_el_hiatus:nnnw #1#2#3#4 \q_@@_recursion_stop { \@@_change_case_store:e { \@@_change_case_codepoint:nn { upper } {#3} } \tl_if_head_is_N_type:nTF {#4} { \@@_change_case_upper_el_hiatus:nnN } { \@@_change_case_loop:nnw } {#1} {#2} #4 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_upper_el_hiatus:nnN #1#2#3 { \token_if_cs:NTF #3 { \@@_change_case_loop:nnw {#1} {#2} #3 } { \@@_codepoint_process:nN { \@@_change_case_upper_el_hiatus:nnn {#1} {#2} } #3 } } \cs_new:Npn \@@_change_case_upper_el_hiatus:nnn #1#2#3 { \@@_change_case_if_takes_dialytika:nTF {#3} { \@@_change_case_upper_el_dialytika:n {#3} \@@_change_case_upper_el_gobble:nnw {#1} {#2} } { \@@_change_case_loop:nnw {#1} {#2} #3 } } % \end{macrocode} % For clearing out trailing combining marks after we have dealt with % the first one. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_el_gobble:nnw #1#2#3 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#3} { \@@_change_case_upper_el_gobble:nnN } { \@@_change_case_loop:nnw } {#1} {#2} #3 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_upper_el_gobble:nnN #1#2#3 { \token_if_cs:NTF #3 { \@@_change_case_loop:nnw {#1} {#2} } { \@@_codepoint_process:nN { \@@_change_case_upper_el_gobble:nnn {#1} {#2} } } #3 } \cs_new:Npn \@@_change_case_upper_el_gobble:nnn #1#2#3 { \bool_lazy_or:nnTF { \@@_change_case_if_greek_accent_p:n {#3} } { \@@_change_case_if_greek_diacritic_p:n {#3} } { \@@_change_case_upper_el_gobble:nnw {#1} {#2} } { \@@_change_case_loop:nnw {#1} {#2} #3 } } % \end{macrocode} % Luckily the Greek range is limited and clear. % \begin{macrocode} \prg_new_conditional:Npnn \@@_change_case_if_greek:n #1 { TF } { \exp_args:Nf \@@_change_case_if_greek:n { \int_eval:n { \@@_codepoint_from_chars:Nw #1 } } } \cs_new:Npn \@@_change_case_if_greek:n #1 { \if_int_compare:w #1 < "0370 \exp_stop_f: \prg_return_false: \else: \if_int_compare:w #1 > "03FF \exp_stop_f: \if_int_compare:w #1 < "1F00 \exp_stop_f: \prg_return_false: \else: \if_int_compare:w #1 > "1FFF \exp_stop_f: \if_int_compare:w #1 = "2126 \exp_stop_f: \prg_return_true: \else: \prg_return_false: \fi: \else: \prg_return_true: \fi: \fi: \else: \prg_return_true: \fi: \fi: } % \end{macrocode} % We follow ICU in adding a few extras to the accent list here. % \begin{macrocode} \prg_new_conditional:Npnn \@@_change_case_if_greek_accent:n #1 { TF , p } { \exp_args:Nf \@@_change_case_if_greek_accent:n { \int_eval:n { \@@_codepoint_from_chars:Nw #1 } } } \cs_new:Npn \@@_change_case_if_greek_accent:n #1 { \if_int_compare:w #1 = "0300 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0301 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0342 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0302 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0303 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0311 \exp_stop_f: \prg_return_true: \else: \prg_return_false: \fi: \fi: \fi: \fi: \fi: \fi: } \prg_new_conditional:Npnn \@@_change_case_if_greek_diacritic:n #1 { TF , p } { \exp_args:Nf \@@_change_case_if_greek_diacritic:n { \int_eval:n { \@@_codepoint_from_chars:Nw #1 } } } \cs_new:Npn \@@_change_case_if_greek_diacritic:n #1 { \if_int_compare:w #1 = "0304 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0306 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0313 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0314 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "0343 \exp_stop_f: \prg_return_true: \else: \prg_return_false: \fi: \fi: \fi: \fi: \fi: } \prg_new_conditional:Npnn \@@_change_case_if_takes_dialytika:n #1 { TF } { \exp_args:Nf \@@_change_case_if_takes_dialytika:n { \int_eval:n { \@@_codepoint_from_chars:Nw #1 } } } \cs_new:Npn \@@_change_case_if_takes_dialytika:n #1 { \if_int_compare:w #1 = "0399 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "03B9 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "03A5 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "03C5 \exp_stop_f: \prg_return_true: \else: \prg_return_false: \fi: \fi: \fi: \fi: } \prg_new_conditional:Npnn \@@_change_case_if_takes_ypogegrammeni:n #1 { TF } { \exp_args:Nf \@@_change_case_if_takes_ypogegrammeni:n { \int_eval:n { \@@_codepoint_from_chars:Nw #1 } } } \cs_new:Npn \@@_change_case_if_takes_ypogegrammeni:n #1 { \if_int_compare:w #1 = "03B1 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "03B7 \exp_stop_f: \prg_return_true: \else: \if_int_compare:w #1 = "03C9 \exp_stop_f: \prg_return_true: \else: \prg_return_false: \fi: \fi: \fi: } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \begin{macro}[EXP]{\@@_change_case_boundary_upper_el:Nnnw} % \begin{macro}[EXP]{\@@_change_case_boundary_upper_el:nnN} % \begin{macro}[EXP]{\@@_change_case_boundary_upper_el:nnn} % \begin{macro}[EXP]{\@@_change_case_boundary_upper_el:nnnw} % \begin{macro}[EXP]{\@@_change_case_boundary_upper_el:nnnN} % There is one special case in Greek that needs to be picked up based on % being an isolated letter. We do that using a test similar to final sigma, % but it has to fire off from the space grabber. % \begin{macrocode} \cs_new:Npn \@@_change_case_boundary_upper_el:Nnnw #1#2#3#4 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#4} { \@@_change_case_boundary_upper_el:nnN } { \@@_change_case_loop:nnw } {#2} {#3} #4 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_boundary_upper_el:nnN #1#2#3 { \token_if_cs:NTF #3 { \@@_change_case_loop:nnw {#1} {#2} } { \@@_codepoint_process:nN { \@@_change_case_boundary_upper_el:nnn {#1} {#2} } } #3 } \cs_new:Npn \@@_change_case_boundary_upper_el:nnn #1#2#3 { \bool_lazy_or:nnTF { \@@_codepoint_compare_p:nNn {#3} = { "03AE } } { \@@_codepoint_compare_p:nNn {#3} = { "1F22 } } { \@@_change_case_boundary_upper_el:nnnw {#1} {#2} {#3} } { \@@_change_case_loop:nnw {#1} {#2} #3 } } \cs_new:Npn \@@_change_case_boundary_upper_el:nnnw #1#2#3#4 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#4} { \@@_change_case_boundary_upper_el:nnnN {#3} } { \@@_change_case_store:e { \codepoint_generate:nn { "0389 } { \@@_change_case_catcode:nn {#3} { "0389 } } } \@@_change_case_loop:nnw } {#1} {#2} #4 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_boundary_upper_el:nnnN #1#2#3#4 { \bool_lazy_or:nnTF { \token_if_letter_p:N #4 } { \bool_lazy_and_p:nn { \token_if_active_p:N #4 } { ! \int_compare_p:nNn {`#4} < { "80 } } } { \@@_change_case_loop:nnw {#2} {#3} #1#4 } { \@@_change_case_store:e { \codepoint_generate:nn { "0389 } { \@@_change_case_catcode:nn {#1} { "0389 } } } \@@_change_case_loop:nnw {#2} {#3} #4 } } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \begin{macro}[EXP]{\@@_change_case_title_el:nnnn} % Titlecasing retains accents, but to prevent the uppercasing code % from kicking in, there has to be an explicit function here. % \begin{macrocode} \cs_new:Npn \@@_change_case_title_el:nnnn #1#2#3#4 { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } % \end{macrocode} % \end{macro} % % \begin{macro}[EXP] % { % \@@_change_case_upper_hy:nnnn , % \@@_change_case_title_hy:nnnn , % \@@_change_case_upper_hy-x-yiwn:nnnn , % \@@_change_case_title_hy-x-yiwn:nnnn % } % See \url{https://www.unicode.org/L2/L2020/20143-armenian-ech-yiwn.pdf}. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_hy:nnnn #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "0587 } { \@@_change_case_store:e { \codepoint_generate:nn { "0535 } { \@@_change_case_catcode:nn {#4} { "0535 } } \codepoint_generate:nn { "054E } { \@@_change_case_catcode:nn {#4} { "054E } } } \use:c { @@_change_case_next_ #2 :nn } {#2} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } \cs_new:Npn \@@_change_case_title_hy:nnnn #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "0587 } { \@@_change_case_store:e { \codepoint_generate:nn { "0535 } { \@@_change_case_catcode:nn {#4} { "0535 } } \codepoint_generate:nn { "057E } { \@@_change_case_catcode:nn {#4} { "057E } } } \use:c { @@_change_case_next_ #2 :nn } {#2} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } \cs_new:cpn { @@_change_case_upper_hy-x-yiwn:nnnn } #1#2#3#4 { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } \cs_new_eq:cc { @@_change_case_title_hy-x-yiwn:nnnn } { @@_change_case_upper_hy-x-yiwn:nnnn } % \end{macrocode} % \end{macro} % % \begin{macro}[EXP]{\@@_change_case_lower_la-x-medieval:nnnn} % \begin{macro}[EXP]{\@@_change_case_upper_la-x-medieval:nnnn} % Simply swaps of characters. % \begin{macrocode} \cs_new:cpn { @@_change_case_lower_la-x-medieval:nnnn } #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "0056 } { \@@_change_case_store:e { \char_generate:nn { "0075 } { \@@_char_catcode:N #4 } } \use:c { @@_change_case_next_ #2 :nn } {#2} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } \cs_new:cpn { @@_change_case_upper_la-x-medieval:nnnn } #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "0075 } { \@@_change_case_store:e { \char_generate:nn { "0056 } { \@@_char_catcode:N #4 } } \use:c { @@_change_case_next_ #2 :nn } {#2} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } % \end{macrocode} % \end{macro} % \end{macro} % % \begin{macro}[EXP] % { % \@@_change_cases_lower_lt:nnnn , % \@@_change_cases_lower_lt_auxi:nnnn , % \@@_change_cases_lower_lt_auxii:nnnn % } % \begin{macro}[rEXP]{\@@_change_case_lower_lt:nnw} % \begin{macro}[rEXP]{\@@_change_case_lower_lt:nnN} % \begin{macro}[rEXP]{\@@_change_case_lower_lt:nnn} % For Lithuanian, the issue to be dealt with is dots over lower case % letters: these should be present if there is another accent. The first step % is a simple match attempt: look for the three uppercase accented letters % which should gain a dot-above char in their lowercase form. % \begin{macrocode} \cs_new:Npn \@@_change_case_lower_lt:nnnn #1#2#3#4 { \exp_args:Ne \@@_change_case_lower_lt_auxi:nnnn { \int_case:nn { \@@_codepoint_from_chars:Nw #4 } { { "00CC } { "0300 } { "00CD } { "0301 } { "0128 } { "0303 } } } {#2} {#3} {#4} } % \end{macrocode} % If there was a hit, output the result with the dot-above and move on. % Otherwise, look for one of the three letters that can take a combining % accent: I, J and I-ogonek. % \begin{macrocode} \cs_new:Npn \@@_change_case_lower_lt_auxi:nnnn #1#2#3#4 { \tl_if_blank:nTF {#1} { \exp_args:Ne \@@_change_case_lower_lt_auxii:nnnn { \int_case:nn { \@@_codepoint_from_chars:Nw #4 } { { "0049 } { "0069 } { "004A } { "006A } { "012E } { "012F } } } {#2} {#3} {#4} } { \@@_change_case_store:e { \codepoint_generate:nn { "0069 } { \@@_change_case_catcode:nn {#4} { "0069 } } \codepoint_generate:nn { "0307 } { \@@_change_case_catcode:nn {#4} { "0307 } } \codepoint_generate:nn {#1} { \@@_change_case_catcode:nn {#4} {#1} } } \@@_change_case_loop:nnw {#2} {#3} } } % \end{macrocode} % Again, branch depending on a hit. If there is one, we output the character % then need to look for a combining accent: as usual, we need to be aware of % the loop situation. % \begin{macrocode} \cs_new:Npn \@@_change_case_lower_lt_auxii:nnnn #1#2#3#4 { \tl_if_blank:nTF {#1} { \@@_change_case_codepoint:nnnn {#2} {#2} {#3} {#4} } { \@@_change_case_store:e { \codepoint_generate:nn {#1} { \@@_change_case_catcode:nn {#4} {#1} } } \@@_change_case_lower_lt:nnw {#2} {#3} } } \cs_new:Npn \@@_change_case_lower_lt:nnw #1#2#3 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#3} { \@@_change_case_lower_lt:nnN } { \@@_change_case_loop:nnw } {#1} {#2} #3 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_lower_lt:nnN #1#2#3 { \@@_codepoint_process:nN { \@@_change_case_lower_lt:nnn {#1} {#2} } #3 } \cs_new:Npn \@@_change_case_lower_lt:nnn #1#2#3 { \bool_lazy_and:nnT { \bool_lazy_or_p:nn { ! \tl_if_single_p:n {#3} } { ! \token_if_cs_p:N #3 } } { \bool_lazy_any_p:n { { \@@_codepoint_compare_p:nNn {#3} = { "0300 } } { \@@_codepoint_compare_p:nNn {#3} = { "0301 } } { \@@_codepoint_compare_p:nNn {#3} = { "0303 } } } } { \@@_change_case_store:e { \codepoint_generate:nn { "0307 } { \@@_change_case_catcode:nn {#3} { "0307 } } } } \@@_change_case_loop:nnw {#1} {#2} #3 } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \begin{macro}[EXP] % { % \@@_change_cases_upper_lt:nnnn , % \@@_change_cases_upper_lt_aux:nnnn % } % \begin{macro}[rEXP]{\@@_change_case_upper_lt:nnw} % \begin{macro}[rEXP]{\@@_change_case_upper_lt:nnN} % \begin{macro}[rEXP]{\@@_change_case_upper_lt:nnn} % The uppercasing version: first find i/j/i-ogonek, then look for the % combining char: drop it if present. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_lt:nnnn #1#2#3#4 { \exp_args:Ne \@@_change_case_upper_lt_aux:nnnn { \int_case:nn { \@@_codepoint_from_chars:Nw #4 } { { "0069 } { "0049 } { "006A } { "004A } { "012F } { "012E } } } {#2} {#3} {#4} } \cs_new:Npn \@@_change_case_upper_lt_aux:nnnn #1#2#3#4 { \tl_if_blank:nTF {#1} { \@@_change_case_codepoint:nnnn { upper } {#2} {#3} {#4} } { \@@_change_case_store:e { \codepoint_generate:nn {#1} { \@@_change_case_catcode:nn {#4} {#1} } } \@@_change_case_upper_lt:nnw {#2} {#3} } } \cs_new:Npn \@@_change_case_upper_lt:nnw #1#2#3 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#3} { \@@_change_case_upper_lt:nnN } { \use:c { @@_change_case_next_ #1 :nn } } {#1} {#2} #3 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_upper_lt:nnN #1#2#3 { \@@_codepoint_process:nN { \@@_change_case_upper_lt:nnn {#1} {#2} } #3 } \cs_new:Npn \@@_change_case_upper_lt:nnn #1#2#3 { \bool_lazy_and:nnTF { \bool_lazy_or_p:nn { ! \tl_if_single_p:n {#3} } { ! \token_if_cs_p:N #3 } } { \@@_codepoint_compare_p:nNn {#3} = { "0307 } } { \use:c { @@_change_case_next_ #1 :nn } {#1} {#2} } { \use:c { @@_change_case_next_ #1 :nn } {#1} {#2} #3 } } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % % \begin{macro}[EXP] % {\@@_change_case_title_nl:nnnn, \@@_change_case_title_nl_aux:nnnn} % \begin{macro}[EXP]{\@@_change_case_title_nl:nnw} % \begin{macro}[EXP]{\@@_change_case_title_nl:nnN} % For Dutch, there is a single look-ahead test for \texttt{ij} when % title casing. If the appropriate letters are found, produce \texttt{IJ} % and gobble the \texttt{j}/\texttt{J}. % \begin{macrocode} \cs_new:Npn \@@_change_case_title_nl:nnnn #1#2#3#4 { \tl_if_single:nTF {#4} { \@@_change_case_title_nl_aux:nnnn } { \@@_change_case_codepoint:nnnn } {#1} {#2} {#3} {#4} } \cs_new:Npn \@@_change_case_title_nl_aux:nnnn #1#2#3#4 { \bool_lazy_or:nnTF { \int_compare_p:nNn {`#4} = { "0049 } } { \int_compare_p:nNn {`#4} = { "0069 } } { \@@_change_case_store:e { \char_generate:nn { "0049 } { \@@_char_catcode:N #4 } } \@@_change_case_title_nl:nnw {#2} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } \cs_new:Npn \@@_change_case_title_nl:nnw #1#2#3 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#3} { \@@_change_case_title_nl:nnN } { \use:c { @@_change_case_next_ #1 :nn } } {#1} {#2} #3 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_title_nl:nnN #1#2#3 { \bool_lazy_and:nnTF { ! \token_if_cs_p:N #3 } { \bool_lazy_or_p:nn { \int_compare_p:nNn {`#3} = { "004A } } { \int_compare_p:nNn {`#3} = { "006A } } } { \@@_change_case_store:e { \char_generate:nn { "004A } { \@@_char_catcode:N #3 } } \use:c { @@_change_case_next_ #1 :nn } {#1} {#2} } { \use:c { @@_change_case_next_ #1 :nn } {#1} {#2} #3 } } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % % \begin{macro}[EXP]{\@@_change_case_lower_tr:nnnn} % \begin{macro}[EXP]{\@@_change_case_lower_tr:nnNw} % \begin{macro}[EXP]{\@@_change_case_lower_tr:NnnN} % \begin{macro}[EXP]{\@@_change_case_lower_tr:Nnnn} % The Turkic languages need special treatment for dotted-i and dotless-i. % The lower casing rule can be expressed in terms of searching first for % either a dotless-I or a dotted-I. In the latter case the mapping is % easy, but in the former there is a second stage search. % \begin{macrocode} \cs_new:Npn \@@_change_case_lower_tr:nnnn #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "0049 } { \@@_change_case_lower_tr:nnNw {#1} {#3} #4 } { \@@_codepoint_compare:nNnTF {#4} = { "0130 } { \@@_change_case_store:e { \codepoint_generate:nn { "0069 } { \@@_change_case_catcode:nn {#4} { "0069 } } } \@@_change_case_loop:nnw {#1} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } } % \end{macrocode} % After a dotless-I there may be a dot-above character. If there is then % a dotted-i should be produced, otherwise output a dotless-i. When the % combination is found both the dotless-I and the dot-above char have to % be removed from the input. % \begin{macrocode} \cs_new:Npn \@@_change_case_lower_tr:nnNw #1#2#3#4 \q_@@_recursion_stop { \tl_if_head_is_N_type:nTF {#4} { \@@_change_case_lower_tr:NnnN #3 {#1} {#2} } { \@@_change_case_store:e { \codepoint_generate:nn { "0131 } { \@@_change_case_catcode:nn {#3} { "0131 } } } \@@_change_case_loop:nnw {#1} {#2} } #4 \q_@@_recursion_stop } \cs_new:Npn \@@_change_case_lower_tr:NnnN #1#2#3#4 { \@@_codepoint_process:nN { \@@_change_case_lower_tr:Nnnn #1 {#2} {#3} } #4 } \cs_new:Npn \@@_change_case_lower_tr:Nnnn #1#2#3#4 { \bool_lazy_or:nnTF { \bool_lazy_and_p:nn { \tl_if_single_p:n {#4} } { \token_if_cs_p:N #4 } } { ! \@@_codepoint_compare_p:nNn {#4} = { "0307 } } { \@@_change_case_store:e { \codepoint_generate:nn { "0131 } { \@@_change_case_catcode:nn {#1} { "0131 } } } \@@_change_case_loop:nnw {#2} {#3} #4 } { \@@_change_case_store:e { \codepoint_generate:nn { "0069 } { \@@_change_case_catcode:nn {#1} { "0069 } } } \@@_change_case_loop:nnw {#2} {#3} } } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \begin{macro}[EXP]{\@@_change_case_upper_tr:nnnn} % Uppercasing is easier: just one exception with no context. % \begin{macrocode} \cs_new:Npn \@@_change_case_upper_tr:nnnn #1#2#3#4 { \@@_codepoint_compare:nNnTF {#4} = { "0069 } { \@@_change_case_store:e { \codepoint_generate:nn { "0130 } { \@@_change_case_catcode:nn {#4} { "0130 } } } \use:c { @@_change_case_next_ #2 :nn } {#2} {#3} } { \@@_change_case_codepoint:nnnn {#1} {#2} {#3} {#4} } } % \end{macrocode} % \end{macro} % % \begin{macro}[EXP] % {\@@_change_case_lower_az:nnnn, \@@_change_case_upper_az:nnnn} % Straight copies. % \begin{macrocode} \cs_new_eq:NN \@@_change_case_lower_az:nnnn \@@_change_case_lower_tr:nnnn \cs_new_eq:NN \@@_change_case_upper_az:nnnn \@@_change_case_upper_tr:nnnn % \end{macrocode} % \end{macro} % % The (fixed) look-up mappings for letter-like control sequences. % \begin{macrocode} \group_begin: \cs_set_protected:Npn \@@_change_case_setup:NN #1#2 { \quark_if_recursion_tail_stop:N #1 \tl_const:cn { c_@@_lowercase_ \token_to_str:N #1 _tl } { #2 } \tl_const:cn { c_@@_uppercase_ \token_to_str:N #2 _tl } { #1 } \@@_change_case_setup:NN } \@@_change_case_setup:NN \AA \aa \AE \ae \DH \dh \DJ \dj \IJ \ij \L \l \NG \ng \O \o \OE \oe \SS \ss \TH \th \q_recursion_tail ? \q_recursion_stop \tl_const:cn { c_@@_uppercase_ \token_to_str:N \i _tl } { I } \tl_const:cn { c_@@_uppercase_ \token_to_str:N \j _tl } { J } \group_end: % \end{macrocode} % % To deal with possible encoding-specific extensions to \tn{@uclclist}, % we check at the end of the preamble. This will therefore only apply % to \LaTeXe{} package mode. % \begin{macrocode} \tl_if_exist:NT \@expl@finalise@setup@@@@ { \tl_gput_right:Nn \@expl@finalise@setup@@@@ { \tl_gput_right:Nn \@kernel@after@begindocument { \group_begin: \cs_set_protected:Npn \@@_change_case_setup:Nn #1#2 { \quark_if_recursion_tail_stop:N #1 \tl_if_single_token:nT {#2} { \cs_if_exist:cF { c_@@_uppercase_ \token_to_str:N #1 _tl } { \tl_const:cn { c_@@_uppercase_ \token_to_str:N #1 _tl } { #2 } } \cs_if_exist:cF { c_@@_lowercase_ \token_to_str:N #2 _tl } { \tl_const:cn { c_@@_lowercase_ \token_to_str:N #2 _tl } { #1 } } } \@@_change_case_setup:Nn } \exp_after:wN \@@_change_case_setup:Nn \@uclclist \q_recursion_tail ? \q_recursion_stop \group_end: } } } % \end{macrocode} % % \begin{macrocode} % % \end{macrocode} % % \end{implementation} % % \PrintIndex