% Copyright (C) 2026 Yu Zhai % This work may be distributed and/or modified under the LaTeX Project % Public License, version 1.3c or (at your option) any later version. % https://www.latex-project.org/lppl.txt % LPPL maintenance status: maintained. % Current Maintainer: Yu Zhai . % The distributed Work is listed in MANIFEST.md. % \NeedsTeXFormat{LaTeX2e}[2020-10-01] \IfFormatAtLeastTF{2020-10-01}{}{% \PackageError{matchedsplit}{LaTeX 2020-10-01 or newer is required}% {Update your LaTeX format and packages before loading matchedsplit.}% \endinput } \ProvidesExplPackage{matchedsplit}{2026-10-29}{0.8.1}{Nested matched delimiters across aligned rows} \RequirePackage{amsmath} \RequirePackage{xcolor} \RequirePackage{l3keys2e} \ExplSyntaxOn \bool_new:N \g__ms_override_split_bool \bool_new:N \g__ms_override_align_bool \bool_new:N \g__ms_rainbow_bool \bool_new:N \l__ms_rainbow_bool \clist_new:N \g__ms_colors_clist \clist_new:N \l__ms_colors_clist \prop_new:N \l__ms_color_prop \definecolor{msblue}{HTML}{0072B2} \definecolor{msorange}{HTML}{D55E00} \definecolor{msgreen}{HTML}{009E73} \definecolor{mspurple}{HTML}{9B51A0} \definecolor{msred}{HTML}{B33A3A} \definecolor{mscyan}{HTML}{00838F} \clist_gset:Nn \g__ms_colors_clist {msblue,msorange,msgreen,mspurple,msred,mscyan} \msg_new:nnn {matchedsplit}{empty-colors}{The~colors~list~must~not~be~empty.} \cs_new_protected:Npn \__ms_set_colors:Nn #1#2 {\clist_set:Nn \l_tmpa_clist {#2} \clist_if_empty:NTF \l_tmpa_clist {\msg_error:nn {matchedsplit}{empty-colors}} {\clist_set_eq:NN #1\l_tmpa_clist}} \keys_define:nn {matchedsplit/package} { overridesplit .bool_gset:N = \g__ms_override_split_bool, overridealign .bool_gset:N = \g__ms_override_align_bool, rainbow .bool_gset:N = \g__ms_rainbow_bool, colors .code:n = {\__ms_set_colors:Nn \l__ms_colors_clist {#1} \clist_gset_eq:NN \g__ms_colors_clist \l__ms_colors_clist} } \clist_set_eq:NN \l__ms_colors_clist \g__ms_colors_clist \ProcessKeysOptions{matchedsplit/package} \keys_define:nn {matchedsplit/environment} { rainbow .bool_set:N = \l__ms_rainbow_bool, colors .code:n = {\__ms_set_colors:Nn \l__ms_colors_clist {#1}} } \cs_new_protected:Npn \__ms_options:n #1 {\bool_set_eq:NN \l__ms_rainbow_bool \g__ms_rainbow_bool \clist_set_eq:NN \l__ms_colors_clist \g__ms_colors_clist \keys_set:nn {matchedsplit/environment}{#1}} \cs_generate_variant:Nn \tl_if_head_is_group:nTF {V} \cs_generate_variant:Nn \tl_if_head_eq_charcode:nNT {VN} \tl_new:N \l__ms_input_tl \tl_new:N \l__ms_render_tl \tl_new:N \l__ms_content_tl \tl_new:N \l__ms_key_tl \seq_new:N \l__ms_stack_seq \int_new:N \l__ms_id_int \int_new:N \l__ms_pending_int \int_new:N \l__ms_env_depth_int \int_new:N \l__ms_env_pending_int \prop_new:N \l__ms_content_prop \bool_new:N \l__ms_valid_bool \msg_new:nnn {matchedsplit} {unmatched} {Unmatched~#1~in~matchedsplit.} \msg_new:nnn {matchedsplit} {metadata} {Expected~a~braced~argument~after~label/tag/intertext.} \msg_new:nnn {matchedsplit} {row-space} {Missing~closing~bracket~in~row~spacing.} \msg_new:nnn {matchedsplit} {delimiter} {Missing~delimiter~after~left/middle/right.} \cs_new_protected:Npn \__ms_fail:n #1 {\bool_set_false:N \l__ms_valid_bool \msg_error:nnn {matchedsplit}{unmatched}{#1}} \cs_new_protected:Npn \__ms_append_active:n #1 {\seq_map_inline:Nn \l__ms_stack_seq {\prop_get:NnN \l__ms_content_prop {##1} \l__ms_content_tl \tl_put_right:Nn \l__ms_content_tl {#1} \prop_put:NnV \l__ms_content_prop {##1} \l__ms_content_tl}} \cs_new_protected:Npn \__ms_close:n #1 { \seq_pop_right:NN \l__ms_stack_seq \l__ms_key_tl \prop_get:NVN \l__ms_content_prop \l__ms_key_tl \l__ms_content_tl \box_if_exist:cF {l__ms_delim_\l__ms_key_tl _box} {\box_new:c {l__ms_delim_\l__ms_key_tl _box}} % Invisible delimiters support middle without inflating the measured height. \hbox_set:cn {l__ms_delim_\l__ms_key_tl _box} {$\displaystyle \left.\l__ms_content_tl\right.$} \tl_put_right:Nx \l__ms_render_tl {\exp_not:N \__ms_right:nn {\l__ms_key_tl}{\exp_not:n{#1}}} \__ms_append_active:n {\right#1} } \cs_new_protected:Npn \__ms_token:n #1 { \int_compare:nNnTF {\l__ms_pending_int} > {0} { \int_case:nn {\l__ms_pending_int} { {1}{\__ms_append_active:n {\left#1} \int_incr:N \l__ms_id_int \tl_set:Nx \l__ms_key_tl {\int_use:N \l__ms_id_int} \seq_put_right:NV \l__ms_stack_seq \l__ms_key_tl \prop_put:NVx \l__ms_color_prop \l__ms_key_tl {\int_eval:n {\int_mod:nn {\seq_count:N \l__ms_stack_seq - 1} {\clist_count:N \l__ms_colors_clist} + 1}} \prop_put:NVn \l__ms_content_prop \l__ms_key_tl {} \tl_put_right:Nx \l__ms_render_tl {\exp_not:N \__ms_left:nn {\l__ms_key_tl}{\exp_not:n{#1}}}} {2}{\seq_if_empty:NTF \l__ms_stack_seq {\__ms_fail:n {middle}} {\seq_get_right:NN \l__ms_stack_seq \l__ms_key_tl \tl_put_right:Nx \l__ms_render_tl {\exp_not:N \__ms_middle:nn {\l__ms_key_tl}{\exp_not:n{#1}}} \__ms_append_active:n {\middle#1}}} {3}{\seq_if_empty:NTF \l__ms_stack_seq {\__ms_fail:n {right}} {\__ms_close:n {#1}}} } \int_zero:N \l__ms_pending_int } { \token_if_eq_meaning:NNTF #1 \left {\int_set:Nn \l__ms_pending_int {1}} {\token_if_eq_meaning:NNTF #1 \middle {\int_set:Nn \l__ms_pending_int {2}} {\token_if_eq_meaning:NNTF #1 \right {\int_set:Nn \l__ms_pending_int {3}} {\tl_put_right:Nn \l__ms_render_tl {#1} \token_if_eq_meaning:NNTF #1 \\ {\__ms_row_options:} {\token_if_eq_charcode:NNF #1 & {\__ms_append_active:n {#1}}}}}} } } % Preserve row spacing in the output, but remove it from the measurement. \cs_new_protected:Npn \__ms_row_options: { \tl_trim_spaces:N \l__ms_input_tl \tl_if_head_eq_charcode:VNT \l__ms_input_tl * {\tl_put_right:Nn \l__ms_render_tl {*} \tl_set:Nx \l__ms_input_tl {\exp_not:f {\tl_tail:V \l__ms_input_tl}}} \tl_trim_spaces:N \l__ms_input_tl \tl_if_head_eq_charcode:VNT \l__ms_input_tl [ {\tl_if_in:NnTF \l__ms_input_tl {]} {\exp_after:wN \__ms_row_space:w \l__ms_input_tl \q_mark \q_stop} {\bool_set_false:N \l__ms_valid_bool \tl_clear:N \l__ms_input_tl \msg_error:nn {matchedsplit}{row-space}}} } \cs_new_protected:Npn \__ms_row_space:w [#1]#2\q_stop {\tl_put_right:Nn \l__ms_render_tl {[#1]} \tl_set:Nn \l__ms_input_tl {#2}\tl_remove_once:Nn \l__ms_input_tl {\q_mark}} % Groups are opaque: keep braces and native math environments intact. \cs_new_protected:Npn \__ms_scan: { \tl_trim_spaces:N \l__ms_input_tl \tl_if_empty:NF \l__ms_input_tl {\tl_if_head_is_group:VTF \l__ms_input_tl {\exp_after:wN \__ms_group:w \l__ms_input_tl \q_mark \q_stop} {\exp_after:wN \__ms_single:w \l__ms_input_tl \q_mark \q_stop} \__ms_scan:} } \cs_new_protected:Npn \__ms_group:w #1#2\q_stop {\tl_set:Nn \l__ms_input_tl {#2}\tl_remove_once:Nn \l__ms_input_tl {\q_mark} \int_compare:nNnTF {\l__ms_pending_int} = {0} {\int_add:Nn \l__ms_env_depth_int {\l__ms_env_pending_int} \int_zero:N \l__ms_env_pending_int \__ms_append_active:n {{#1}} \tl_put_right:Nn \l__ms_render_tl {{#1}}} {\bool_set_false:N \l__ms_valid_bool \msg_error:nn {matchedsplit}{delimiter} \int_zero:N \l__ms_pending_int}} \cs_new_protected:Npn \__ms_single:w #1#2\q_stop {\tl_set:Nn \l__ms_input_tl {#2}\tl_remove_once:Nn \l__ms_input_tl {\q_mark} \__ms_dispatch:n {#1}} % Keep nested environments opaque; their alignment tokens belong to them. \cs_new_protected:Npn \__ms_copy:n #1 {\__ms_append_active:n {#1}\tl_put_right:Nn \l__ms_render_tl {#1}} \cs_new_protected:Npn \__ms_dispatch:n #1 { \token_if_eq_meaning:NNTF #1 \begin {\int_set:Nn \l__ms_env_pending_int {1}\__ms_copy:n {#1}} {\token_if_eq_meaning:NNTF #1 \end {\int_set:Nn \l__ms_env_pending_int {-1}\__ms_copy:n {#1}} {\int_compare:nNnTF {\l__ms_env_depth_int} > {0} {\__ms_copy:n {#1}} {\token_if_eq_meaning:NNTF #1 \label {\__ms_metadata:n {#1}} {\token_if_eq_meaning:NNTF #1 \tag {\__ms_metadata:n {#1}} {\token_if_eq_meaning:NNTF #1 \notag {\tl_put_right:Nn \l__ms_render_tl {#1}} {\token_if_eq_meaning:NNTF #1 \nonumber {\tl_put_right:Nn \l__ms_render_tl {#1}} {\token_if_eq_meaning:NNTF #1 \intertext {\__ms_metadata:n {#1}} {\token_if_eq_meaning:NNTF #1 \displaybreak {\tl_put_right:Nn \l__ms_render_tl {#1}\__ms_row_options:} {\__ms_token:n {#1}}}}}}}}}} } % Labels and tags must run only during amsmath's numbering pass, never % while measuring delimiter content (which may span several numbered rows). \cs_new_protected:Npn \__ms_metadata:n #1 { \tl_put_right:Nn \l__ms_render_tl {#1} \tl_trim_spaces:N \l__ms_input_tl \token_if_eq_meaning:NNT #1 \tag {\tl_if_head_eq_charcode:VNT \l__ms_input_tl * {\tl_put_right:Nn \l__ms_render_tl {*} \tl_set:Nx \l__ms_input_tl {\exp_not:f {\tl_tail:V \l__ms_input_tl}}}} \tl_trim_spaces:N \l__ms_input_tl \tl_if_head_is_group:VTF \l__ms_input_tl {\exp_after:wN \__ms_metadata_arg:w \l__ms_input_tl \q_mark \q_stop} {\bool_set_false:N \l__ms_valid_bool \tl_clear:N \l__ms_input_tl \msg_error:nn {matchedsplit}{metadata}} } \cs_new_protected:Npn \__ms_metadata_arg:w #1#2\q_stop {\tl_put_right:Nn \l__ms_render_tl {{#1}} \tl_set:Nn \l__ms_input_tl {#2}\tl_remove_once:Nn \l__ms_input_tl {\q_mark}} \cs_new_protected:Npn \__ms_color:n #1 {\bool_if:NT \l__ms_rainbow_bool {\exp_args:Nx \color {\clist_item:Nn \l__ms_colors_clist {\prop_item:Nn \l__ms_color_prop {#1}}}}} \cs_new:Npn \__ms_phantom:n #1 {\vphantom{\box_use:c {l__ms_delim_#1_box}}} \cs_new:Npn \__ms_left:nn #1#2 {\mathopen{\__ms_color:n {#1}\nulldelimiterspace=0pt \left#2\__ms_phantom:n{#1}\right.}} \cs_new:Npn \__ms_middle:nn #1#2 {\mathrel{\__ms_color:n {#1}\nulldelimiterspace=0pt \left.\__ms_phantom:n{#1}\middle#2\right.}} \cs_new:Npn \__ms_right:nn #1#2 {\mathclose{\__ms_color:n {#1}\nulldelimiterspace=0pt \left.\__ms_phantom:n{#1}\right#2}} \cs_new_protected:Npn \__ms_prepare:n #1 { \tl_set:Nn \l__ms_input_tl {#1} \tl_clear:N \l__ms_render_tl \seq_clear:N \l__ms_stack_seq \prop_clear:N \l__ms_content_prop \prop_clear:N \l__ms_color_prop \int_zero:N \l__ms_env_depth_int \int_zero:N \l__ms_env_pending_int \int_zero:N \l__ms_id_int \int_zero:N \l__ms_pending_int \bool_set_true:N \l__ms_valid_bool \__ms_scan: \int_compare:nNnF {\l__ms_pending_int} = {0} {\bool_set_false:N \l__ms_valid_bool \msg_error:nn {matchedsplit}{delimiter}} \seq_if_empty:NF \l__ms_stack_seq {\__ms_fail:n {left}} } \cs_new_protected:Npn \__ms_typeset:nnn #1#2#3 {\group_align_safe_begin: \group_begin: \__ms_options:n {#3} \__ms_prepare:n {#1} \bool_if:NT \l__ms_valid_bool {\begin{aligned}[#2]\tl_use:N \l__ms_render_tl\end{aligned}} \group_end:\group_align_safe_end:} \NewDocumentEnvironment{matchedsplit}{O{} +b} {\__ms_typeset:nnn {#2}{c}{#1}}{} \bool_if:NT \g__ms_override_split_bool { \RenewDocumentEnvironment{split}{O{} +b} {\ifctagsplit@ \__ms_typeset:nnn {#2}{c}{#1} \else \iftagsleft@ \__ms_typeset:nnn {#2}{t}{#1} \else \__ms_typeset:nnn {#2}{b}{#1}\fi \fi}{} % amsmath locally restores split from insplit@ inside align and gather. \cs_set_eq:NN \insplit@ \split } % Retain native amsmath numbering, measuring, tags and display layout. \NewCommandCopy\msnativealign\align \NewCommandCopy\endmsnativealign\endalign \exp_args:Ncc \NewCommandCopy {msnativealign*}{align*} \exp_args:Ncc \NewCommandCopy {endmsnativealign*}{endalign*} \cs_new_protected:Npn \__ms_align:nnn #1#2#3 {\group_begin: \group_align_safe_begin: \__ms_options:n {#3} \__ms_prepare:n {#1} \group_align_safe_end: \bool_if:NT \l__ms_valid_bool {\exp_args:NnV \__ms_align_render:nn {#2}\l__ms_render_tl} \group_end:} \cs_new_protected:Npn \__ms_align_render:nn #1#2 {\cs_set_eq:NN \endalign \endmsnativealign \begin{msnativealign#1}#2\end{msnativealign#1}} \NewDocumentEnvironment{matchedalign}{O{} +b}{\__ms_align:nnn {#2}{}{#1}}{} \NewDocumentEnvironment{matchedalign*}{O{} +b}{\__ms_align:nnn {#2}{*}{#1}}{} \bool_if:NT \g__ms_override_align_bool { \RenewDocumentEnvironment{align}{O{} +b}{\__ms_align:nnn {#2}{}{#1}}{} \RenewDocumentEnvironment{align*}{O{} +b}{\__ms_align:nnn {#2}{*}{#1}}{} % These native environments also finish through endalign. Restore its % native meaning locally so the opt-in replacement cannot break them. \clist_map_inline:nn {alignat,alignat*,xalignat,xalignat*,xxalignat,flalign,flalign*} {\AddToHook{env/#1/begin}{\cs_set_eq:NN \endalign \endmsnativealign}} } \ExplSyntaxOff