% % topiclongtable v1.3.2 - Renders autocollapsing cells in longtables % % Copyright (c) 2017-2020 Paolo Brasolin () % % This work is sponsored by Human Predictions, LLC (). % This work is maintained by Paolo Brasolin (). % This work is available under the terms of the MIT License. % \NeedsTeXFormat{LaTeX2e}[2017-04-15] \RequirePackage{zref-abspage}[2016/05/21] \RequirePackage{xparse}[2017/11/14] \RequirePackage{expl3}[2017/11/14] \RequirePackage{array}[2016/10/06] \RequirePackage{multirow}[2016/11/25] \RequirePackage{longtable}[2014/10/28] \PassOptionsToPackage{longtable}{multirow} \ProvidesExplPackage {topiclongtable} {2020/04/12} {v1.3.2} {Renders autocollapsing cells in longtables} \ProcessOptions\relax %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% topics bookeeping (current table) \seq_new:N \g_tlt_topics_stack_seq \prop_new:N \g_tlt_topics_labels_prop %% tracks row spans of topics (for computation) \prop_new:N \g_tlt_rows_spans_prop %% holds computed multirow heights (for rendering) \clist_new:N \g_tlt_multirows_heights_clist %% holds dumped data \prop_new:N \g_tlt_raw_data_prop %% holds dumped data (parsed for eas of access) \prop_new:N \g_tlt_loaded_data_prop %% hold obvious col/row indices \int_new:N \g_tlt_row_idx_int \int_new:N \g_tlt_col_idx_int \int_new:N \g_tlt_col_tot_int %% io stream \iow_new:N \g_tlt_stream_iow %% tracks table index (id) \tl_new:N \g_tlt_cur_table_id_tl %% hold continuation code \tl_new:N \g_tlt_continuation_code_tl %% hold multirow options \tl_new:N \g_tlt_multirow_vpos_tl \tl_new:N \g_tlt_multirow_width_tl %% whose defaults are \tl_gset:Nn \g_tlt_multirow_vpos_tl {t} \tl_gset:Nn \g_tlt_multirow_width_tl {*} %% just a handy helper \cs_generate_variant:Nn \prop_item:Nn { NV } %% INIT PROCEDURES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% initialize the document \cs_new:Nn \tlt_init: { %% load computed data if it exists \tl_set:Nn \l_tmpa_tl { \c_sys_jobname_str.tlt } \file_if_exist:nTF { \l_tmpa_tl } { \ior_open:Nn \g_tlt_stream_iow { \l_tmpa_tl } \ior_map_inline:Nn \g_tlt_stream_iow { ##1 } \ior_close:N \g_tlt_stream_iow } { %% file was not found - that's ok } %% leave an open stream to dump computed data \iow_open:Nn \g_tlt_stream_iow { \c_sys_jobname_str.tlt } } %% initialize a table with given id \cs_new:Nn \tlt_init_table:n { %% fetch serialized data (clist of clists) if found, %% initialize an empty list otherwise %% NOTE: \g_tlt_raw_data_prop is defined in the (generated) *.tlt file \prop_get:NnNTF \g_tlt_raw_data_prop { #1 } \l_tmpa_tl { \clist_set:NV \l_tmpa_clist \l_tmpa_tl } { \clist_set_eq:NN \l_tmpa_clist \c_empty_clist } %% transform clist into indexed prop for ease of access \int_zero:N \l_tmpa_int \clist_map_inline:Nn \l_tmpa_clist { \int_incr:N \l_tmpa_int \prop_gput:NVn \g_tlt_loaded_data_prop \l_tmpa_int { ##1 } } %% reset counters and table variables \int_gzero:N \g_tlt_col_idx_int \int_gzero:N \g_tlt_row_idx_int \prop_gclear:N \g_tlt_topics_labels_prop \clist_gset:Nn \g_tlt_multirows_heights_clist \c_empty_clist \clist_gset:Nn \g_tlt_rows_spans_prop \c_empty_clist } \cs_generate_variant:Nn \tlt_init_table:n { V } %% EXIT PROCEDURES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% we need to load data through an ordinary macro as at load time we won't have expl3 \DeclareExpandableDocumentCommand{\TltSetDataRow}{mm}{ \prop_gput:Nnn \g_tlt_raw_data_prop { #1 }{ #2 } } %% finalize a given table \cs_new:Nn \tlt_exit_table:n { %% compute table data \tlt_compute_data: %% dump computed data \iow_now:Nx \g_tlt_stream_iow { \noexpand\TltSetDataRow { #1 }{ \g_tlt_multirows_heights_clist } } } \cs_generate_variant:Nn \tlt_exit_table:n { V } %% finalize document \cs_new:Nn \tlt_exit: { %% close the output stream \iow_close:N \g_tlt_stream_iow } %% MULTIROW HEIGHT FETCHING %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \cs_new:Nn \tlt_calc_cell_height:n { %% get loaded data for current column \prop_get:NVNTF \g_tlt_loaded_data_prop \g_tlt_col_idx_int \l_tmpa_tl { \clist_set:NV \l_tmpa_clist \l_tmpa_tl } { \clist_clear:N \l_tmpa_clist } %% get the first element (if present) and 1 otherwise \clist_pop:NNTF \l_tmpa_clist \l_tmpb_tl {} { \tl_set:Nn \l_tmpb_tl { 1 } } \prop_gput:NVV \g_tlt_loaded_data_prop \g_tlt_col_idx_int \l_tmpa_clist \int_set:Nn { #1 } \l_tmpb_tl } %% MULTIROW CONFIGURATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \NewDocumentCommand{\TopicSetVPos}{m}{ \tl_gset:Nn \g_tlt_multirow_vpos_tl {#1} } \NewDocumentCommand{\TopicSetWidth}{m}{ \tl_gset:Nn \g_tlt_multirow_width_tl {#1} } %% TOPICS BOOKKEEPING AND RENDERING %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \NewDocumentCommand{\TopicSetContinuationCode}{m}{ \tl_gset:Nn \g_tlt_continuation_code_tl {#1} } %% internal used to mark/render new topic \cs_new:Nn \mark_new_topic:nn { % reset stack up until #1 \seq_if_in:NnTF \g_tlt_topics_stack_seq {#1} { \bool_do_until:Nn \l_tmpa_bool { \seq_gpop:NN \g_tlt_topics_stack_seq \l_tmpa_tl \prop_gpop:NoN \g_tlt_topics_labels_prop \l_tmpa_tl \l_tmpb_tl \tl_set:Nn \l_tmpb_tl {#1} \bool_gset:Nn \l_tmpa_bool { \tl_if_eq_p:NN \l_tmpa_tl \l_tmpb_tl } } } {} \prop_get:NVNTF \g_tlt_rows_spans_prop \g_tlt_col_idx_int \l_tmpa_tl { \clist_set:NV \l_tmpa_clist \l_tmpa_tl } { \clist_clear:N \l_tmpa_clist } \clist_push:Nn \l_tmpa_clist { 1 } \prop_gput:NVV \g_tlt_rows_spans_prop \g_tlt_col_idx_int \l_tmpa_clist \tlt_calc_cell_height:n \l_tmpb_int \seq_gpush:Nn \g_tlt_topics_stack_seq {#1} \prop_gput:Nnn \g_tlt_topics_labels_prop {#1} {#2} % render the multirow \multirow[\tl_use:N \g_tlt_multirow_vpos_tl]{\int_use:N \l_tmpb_int}{\tl_use:N \g_tlt_multirow_width_tl}{\prop_item:Nn \g_tlt_topics_labels_prop {#1}} } %% internal used to mark/render reappearing topic \cs_new:Nn \mark_old_topic: { \prop_get:NVNTF \g_tlt_rows_spans_prop \g_tlt_col_idx_int \l_tmpa_tl { \clist_set:NV \l_tmpa_clist \l_tmpa_tl } { \ERROR %% unreachable by user by definition } \on_first_row_of_page:N \l_tmpa_bool \bool_if:NTF \l_tmpa_bool { \prop_get:NVNTF \g_tlt_rows_spans_prop \g_tlt_col_idx_int \l_tmpa_tl { \clist_set:NV \l_tmpa_clist \l_tmpa_tl } { \clist_clear:N \l_tmpa_clist } \clist_push:Nn \l_tmpa_clist { 1 } \prop_gput:NVV \g_tlt_rows_spans_prop \g_tlt_col_idx_int \l_tmpa_clist \tlt_calc_cell_height:n \l_tmpb_int %% \seq_gpush:Nn \g_tlt_topics_stack_seq {#1} %% \prop_gput:Nnn \g_tlt_topics_labels_prop {#1} {#2} %% render the multirow cell %% \int_use:N \l_tmpb_int % render multirow \multirow[\tl_use:N \g_tlt_multirow_vpos_tl]{\int_use:N \l_tmpb_int}{\tl_use:N \g_tlt_multirow_width_tl}{\prop_item:NV \g_tlt_topics_labels_prop \g_tlt_col_idx_int \tl_use:N \g_tlt_continuation_code_tl} %% \clist_pop:NN \l_tmpa_clist \l_tmpa_tl %% \int_set:Nn \l_tmpa_int \l_tmpa_tl %% \int_incr:N \l_tmpa_int %% \clist_push:NV \l_tmpa_clist \l_tmpa_int %% \prop_gput:NVV \g_tlt_rows_spans_prop \g_tlt_col_idx_int \l_tmpa_clist %% \tlt_calc_cell_height:n \l_tmpb_int } { \clist_pop:NN \l_tmpa_clist \l_tmpa_tl \int_set:Nn \l_tmpa_int \l_tmpa_tl \int_incr:N \l_tmpa_int \clist_push:NV \l_tmpa_clist \l_tmpa_int \prop_gput:NVV \g_tlt_rows_spans_prop \g_tlt_col_idx_int \l_tmpa_clist \tlt_calc_cell_height:n \l_tmpb_int % render nothing } } %% HIGH LEVEL GET/SET TOPICS OPERATIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \msg_new:nnn {topiclongtable} {invalid-index} { You~tried~to~use~an~uninitialized~topic~index~\msg_line_context:. } % together, the two macros compose the high-level \Topic command \cs_new:Nn \tlt_set_topic:nn { \tl_set:Nn \l_tmpa_tl { #2 } \tl_set:Nx \l_tmpb_tl { \prop_item:Nn \g_tlt_topics_labels_prop { #1 } } \tl_if_eq:NNTF \l_tmpa_tl \l_tmpb_tl { \tlt_get_topic:n { #1 } } { \mark_new_topic:nn {#1} {#2} } } \cs_new:Nn \tlt_get_topic:n { \seq_if_in:NnTF \g_tlt_topics_stack_seq {#1} { \mark_old_topic: } { \msg_error:nn {topiclongtable} {invalid-index} } } \cs_generate_variant:Nn \tlt_get_topic:n { V } \cs_generate_variant:Nn \tlt_set_topic:nn { Vn } %% HEIGHTS COMPUTATIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \cs_new:Nn \tlt_compute_data: { \clist_gclear:N \g_tlt_multirows_heights_clist %% iterate over column indices; idx still has the last (maximum) value \int_step_inline:nnnn { 1 } { 1 } { \g_tlt_col_idx_int } { %% fetch memorized cell spans, ordered from bottom to top \prop_get:NnNTF \g_tlt_rows_spans_prop { ##1 } \l_tmpb_tl { \clist_set:NV \l_tmpa_clist \l_tmpb_tl } { \clist_set:NV \l_tmpa_clist \c_empty_clist } \clist_reverse:N \l_tmpa_clist %% empty tmp list (for outer step cycle) \clist_clear:N \l_tmpb_clist %% iterate over cell spans \clist_map_inline:Nn \l_tmpa_clist { % store column index \int_set:Nn \l_tmpa_int { ####1 } % push full height (n) for first cell of multirow \clist_push:NV \l_tmpb_clist \l_tmpa_int % push n-1 zeroes for the other cells spanned by the multirow \int_while_do:nNnn { \l_tmpa_int } { > } { 1 } { \clist_push:Nn \l_tmpb_clist { 0 } \int_decr:N \l_tmpa_int } } \clist_reverse:N \l_tmpb_clist \clist_gput_right:Nx \g_tlt_multirows_heights_clist {{\l_tmpb_clist}} } } %% CONDITIONALS ON ROW POSITIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \cs_new:Nn \is_last_row_of_page:n { % get page of given row \int_set:Nn \l_tmpa_int { #1 } \tl_set:Nx \l_tmpa_tl {TLT-\theLT@tables-\int_use:N \l_tmpa_int} \tl_set:Nx \l_tmpa_tl {\expandafter\zref@extract{\tl_use:N \l_tmpa_tl}{abspage}} % get page of next row \int_incr:N \l_tmpa_int \tl_set:Nx \l_tmpb_tl {TLT-\theLT@tables-\int_use:N \l_tmpa_int} \tl_set:Nx \l_tmpb_tl {\expandafter\zref@extract{\tl_use:N \l_tmpb_tl}{abspage}} % true if different \bool_not_p:n { \tl_if_eq_p:NN \l_tmpa_tl \l_tmpb_tl } } \cs_new:Nn \is_first_row_of_page:n { % get page of given row \int_set:Nn \l_tmpa_int { #1 } \tl_set:Nx \l_tmpa_tl {TLT-\theLT@tables-\int_use:N \l_tmpa_int} \tl_set:Nx \l_tmpa_tl {\expandafter\zref@extract{\tl_use:N \l_tmpa_tl}{abspage}} % get page of prev row \int_decr:N \l_tmpa_int \tl_set:Nx \l_tmpb_tl {TLT-\theLT@tables-\int_use:N \l_tmpa_int} \tl_set:Nx \l_tmpb_tl {\expandafter\zref@extract{\tl_use:N \l_tmpb_tl}{abspage}} % true if different \bool_set:Nn \l_tmpa_bool { \bool_not_p:n { \tl_if_eq_p:NN \l_tmpa_tl \l_tmpb_tl } } \l_tmpa_bool } \cs_new:Nn \on_first_row_of_page:N { % get page of given row \int_set_eq:NN \l_tmpa_int \g_tlt_row_idx_int \tl_set:Nx \l_tmpa_tl {TLT-\theLT@tables-\int_use:N \l_tmpa_int} \tl_set:Nx \l_tmpa_tl {\expandafter\zref@extract{\tl_use:N \l_tmpa_tl}{abspage}} % get page of prev row \int_decr:N \l_tmpa_int \tl_set:Nx \l_tmpb_tl {TLT-\theLT@tables-\int_use:N \l_tmpa_int} \tl_set:Nx \l_tmpb_tl {\expandafter\zref@extract{\tl_use:N \l_tmpb_tl}{abspage}} % true if different \bool_set:Nn { #1 } { \bool_not_p:n { \tl_if_eq_p:NN \l_tmpa_tl \l_tmpb_tl } } } \cs_new:Nn \on_last_row_of_page:N { % get page of given row \int_set_eq:NN \l_tmpa_int \g_tlt_row_idx_int \tl_set:Nx \l_tmpa_tl {TLT-\theLT@tables-\int_use:N \l_tmpa_int} \tl_set:Nx \l_tmpa_tl {\expandafter\zref@extract{\tl_use:N \l_tmpa_tl}{abspage}} % get page of prev row \int_incr:N \l_tmpa_int \tl_set:Nx \l_tmpb_tl {TLT-\theLT@tables-\int_use:N \l_tmpa_int} \tl_set:Nx \l_tmpb_tl {\expandafter\zref@extract{\tl_use:N \l_tmpb_tl}{abspage}} % true if different \bool_set:Nn { #1 } { \bool_not_p:n { \tl_if_eq_p:NN \l_tmpa_tl \l_tmpb_tl } } } \cs_generate_variant:Nn \is_first_row_of_page:n { V } \cs_generate_variant:Nn \is_last_row_of_page:n { V } %% LINE DRAWING %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \cs_new_protected:Nn \CLINE_split:n { \clist_clear:N \l_tmpb_clist \prop_map_inline:Nn \g_tlt_loaded_data_prop { \tl_set:Nn \l_tmpa_tl {##1} % get first element as integer A \clist_set:Nn \l_tmpa_clist {##2} \clist_pop:NNTF \l_tmpa_clist \l_tmpb_tl { \int_set:Nn \l_tmpa_int \l_tmpb_tl } { \int_set_eq:NN \l_tmpa_int \c_one_int } % if integer A is zero, skip \int_compare:nNnTF { \l_tmpa_int } { = } { 0 } {} { \clist_gpush:NV \l_tmpb_clist \l_tmpa_tl } } \clist_sort:Nn \l_tmpb_clist { \int_compare:nNnTF { ##1 } { > } { ##2 } { \sort_return_swapped: } { \sort_return_same: } } \clist_pop:NNTF \l_tmpb_clist \l_tmpb_tl {} { \tl_set:Nn \l_tmpb_tl { #1 } } \tl_gset:Nx \g_tmpa_tl { \noexpand\cline{\l_tmpb_tl-#1} } \on_last_row_of_page:N \l_tmpa_bool % note the tricky post-group expansion \bool_if:NTF \l_tmpa_bool {} { \group_insert_after:N \g_tmpa_tl } } \cs_generate_variant:Nn \CLINE_split:n { V } %% USER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% used on the first (F) and other (T) columns, hook in the column counter \newcolumntype{F}{>{\int_gset:Nn \g_tlt_col_idx_int {1}}} \newcolumntype{T}{>{\int_gincr:N \g_tlt_col_idx_int}} %% draws the separators, handles the row counter and labels to track pagebreaks \DeclareExpandableDocumentCommand{\TopicLine}{}{ \noalign{ \CLINE_split:V \g_tlt_col_tot_int } \int_gincr:N \g_tlt_row_idx_int \tl_set:Nx \l_tmpa_tl {TLT-\theLT@tables-\int_use:N \g_tlt_row_idx_int} \expandafter\zref@labelbyprops{\tl_use:N \l_tmpa_tl}{abspage} } %% the main environment \NewDocumentEnvironment {topiclongtable} {m} {% %% set next table id \int_set_eq:NN \l_tmpa_int \theLT@tables \int_incr:N \l_tmpa_int \tl_gset:Nx \g_tlt_cur_table_id_tl {TLT\int_use:N \l_tmpa_int} %% initialize table and open environment \tlt_init_table:V \g_tlt_cur_table_id_tl %% \savenotes \begin{longtable}{#1} \noalign{\int_gset:Nn \g_tlt_col_tot_int {\LT@cols}} }{ %% close environment and finalize table \end{longtable} %% \spewnotes \tlt_exit_table:V \g_tlt_cur_table_id_tl } %% the main command \NewDocumentCommand{\Topic}{o}{% \IfValueTF{#1}{\tlt_set_topic:Vn{\g_tlt_col_idx_int}{#1}}{\tlt_get_topic:V{\g_tlt_col_idx_int}}% } %% NOTE: it'd be nice to have \Topic[vpos][width][vmove]{text} %% with the same meanings as \multirow[vpos]{nrows}[bigstruts]{width}[vmove]{text} %% with specification \NewDocumentCommand{\Topic}{D[]{t}D[]{*}D[]{0sp}g} %% but the page breaks make it impossible to predict behaviour, %% especially in relation to vmove - so in essence the settings would end %% up being maximally local (equivalent to handmade multirows). %% HOOKS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% these are needed so set up and tear down io streams \AtBeginDocument{\tlt_init:} \AtEndDocument{\tlt_exit:} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \endinput