% \iffalse meta-comment % Copyright (C) 2026 Andreas Willibald Weber % % This work may be distributed and/or modified under the conditions of the % LaTeX Project Public License, either version 1.3c of this license or (at your % option) any later version. The latest version of the license is available at % https://www.latex-project.org/lppl.txt. % % This work has the LPPL maintenance status `maintained'. The Current % Maintainer of this work is Andreas Willibald Weber. % % This file is part of the `kanban-board' package. The complete list of files % constituting the work appears in the main source file kanban-board.dtx. % \fi % % \begin{macrocode} %<*package> % \end{macrocode} % % \subsection{Palette and column registries} % % Palette names map to lists of heading backgrounds and, for starred % definitions, to paired lists of heading foregrounds. Both maps are global % property lists. Column names are normalized separately and receive % board-local numeric identifiers, so user text never becomes part of an % internal control-sequence name. The registry therefore has two % responsibilities. It maintains the persistent palette catalogue, and it % converts the textual column interface into the compact board-local % representation that the renderers consume. % % \paragraph{Plain palette definitions.} % An unstarred definition accepts a name and a non-empty comma list of % heading backgrounds. A successful definition replaces any palette of the % same name and removes a previously stored foreground list. If the redefined % name is the selected palette, the persistent snapshot is refreshed % immediately so that later boards use the new colors without a second % selection. An invalid definition leaves every registry unchanged. % \begin{macrocode} \cs_new_protected:Npn \__kanban_palette_define:nn #1#2 { \tl_set:Nn \l__kanban_palette_name_tl {#1} \tl_trim_spaces:N \l__kanban_palette_name_tl \tl_if_blank:VTF \l__kanban_palette_name_tl {\msg_warning:nn {kanban-board}{empty-palette-name}} { \clist_set:Nn \l__kanban_palette_colors_clist {#2} \clist_if_empty:NTF \l__kanban_palette_colors_clist {\use:x {\msg_warning:nnnn {kanban-board}{empty-palette} {\exp_not:V \l__kanban_palette_name_tl}{colors}}} { \prop_gput:NVV \g__kanban_palettes_prop \l__kanban_palette_name_tl \l__kanban_palette_colors_clist \prop_gremove:NV \g__kanban_palette_texts_prop \l__kanban_palette_name_tl \tl_if_eq:NNT \l__kanban_palette_name_tl \g__kanban_palette_name_tl { \clist_gset_eq:NN \g__kanban_palette_clist \l__kanban_palette_colors_clist \clist_gclear:N \g__kanban_palette_fg_clist } } } } % \end{macrocode} % % \paragraph{Paired palette definitions.} % The starred syntax associates every heading background with an explicit % heading foreground. Each comma item must contain exactly one |/| separator % and two non-empty components. The item helper validates one pair and % appends its components to the working lists, or records the failure. The % definition is committed only after every pair has passed. A single % malformed item rejects the whole palette rather than installing a partial % result. % \begin{macrocode} \cs_new_protected:Npn \__kanban_palette_pair:n #1 { \seq_set_split:Nnn \l__kanban_palette_pair_seq {/} {#1} \int_compare:nNnTF {\seq_count:N \l__kanban_palette_pair_seq} = {2} { \tl_set:Nx \l__kanban_pair_bg_tl {\seq_item:Nn \l__kanban_palette_pair_seq {1}} \tl_set:Nx \l__kanban_pair_fg_tl {\seq_item:Nn \l__kanban_palette_pair_seq {2}} \tl_trim_spaces:N \l__kanban_pair_bg_tl \tl_trim_spaces:N \l__kanban_pair_fg_tl \bool_if:nTF { \tl_if_blank_p:V \l__kanban_pair_bg_tl || \tl_if_blank_p:V \l__kanban_pair_fg_tl } {\bool_set_true:N \l__kanban_bad_palette_bool} { \clist_put_right:NV \l__kanban_palette_bg_clist \l__kanban_pair_bg_tl \clist_put_right:NV \l__kanban_palette_fg_clist \l__kanban_pair_fg_tl } } {\bool_set_true:N \l__kanban_bad_palette_bool} } \cs_new_protected:Npn \__kanban_palette_define_pairs:nn #1#2 { \tl_set:Nn \l__kanban_palette_name_tl {#1} \tl_trim_spaces:N \l__kanban_palette_name_tl \clist_clear:N \l__kanban_palette_bg_clist \clist_clear:N \l__kanban_palette_fg_clist \bool_set_false:N \l__kanban_bad_palette_bool \tl_if_blank:VTF \l__kanban_palette_name_tl {\msg_warning:nn {kanban-board}{empty-palette-name}} { \clist_set:Nn \l__kanban_palette_colors_clist {#2} \clist_if_empty:NTF \l__kanban_palette_colors_clist {\use:x {\msg_warning:nnnn {kanban-board}{empty-palette} {\exp_not:V \l__kanban_palette_name_tl}{color/text~pairs}}} { \clist_map_function:NN \l__kanban_palette_colors_clist \__kanban_palette_pair:n \bool_if:NTF \l__kanban_bad_palette_bool {\use:x {\msg_warning:nnn {kanban-board}{malformed-palette-pair} {\exp_not:V \l__kanban_palette_name_tl}}} { \prop_gput:NVV \g__kanban_palettes_prop \l__kanban_palette_name_tl \l__kanban_palette_bg_clist \prop_gput:NVV \g__kanban_palette_texts_prop \l__kanban_palette_name_tl \l__kanban_palette_fg_clist \tl_if_eq:NNT \l__kanban_palette_name_tl \g__kanban_palette_name_tl { \clist_gset_eq:NN \g__kanban_palette_clist \l__kanban_palette_bg_clist \clist_gset_eq:NN \g__kanban_palette_fg_clist \l__kanban_palette_fg_clist } } } } } % \end{macrocode} % % \paragraph{Persistent selection and board-local selection.} % Selecting a palette is distinct from defining one. Document-wide setup % replaces the persistent defaults used by future boards. A board option % replaces only the snapshot of the current board. Both selectors verify the % requested name against the same global registry and keep the current % selection when the name is unknown. The board initializer takes the initial % local snapshot when a board begins. % \begin{macrocode} \cs_new_protected:Npn \__kanban_palette_select_global:n #1 { \tl_set:Nn \l__kanban_palette_name_tl {#1} \tl_trim_spaces:N \l__kanban_palette_name_tl \prop_get:NVNTF \g__kanban_palettes_prop \l__kanban_palette_name_tl \l__kanban_palette_bg_tl { \clist_gset:NV \g__kanban_palette_clist \l__kanban_palette_bg_tl \prop_get:NVNTF \g__kanban_palette_texts_prop \l__kanban_palette_name_tl \l__kanban_palette_fg_tl {\clist_gset:NV \g__kanban_palette_fg_clist \l__kanban_palette_fg_tl} {\clist_gclear:N \g__kanban_palette_fg_clist} \tl_gset_eq:NN \g__kanban_palette_name_tl \l__kanban_palette_name_tl } {\use:x {\msg_warning:nnnn {kanban-board}{unknown-palette} {palette}{\exp_not:V \l__kanban_palette_name_tl}}} } \cs_new_protected:Npn \__kanban_palette_select_board:n #1 { \tl_set:Nn \l__kanban_palette_name_tl {#1} \tl_trim_spaces:N \l__kanban_palette_name_tl \prop_get:NVNTF \g__kanban_palettes_prop \l__kanban_palette_name_tl \l__kanban_palette_bg_tl { \clist_set:NV \l__kanban_board_palette_clist \l__kanban_palette_bg_tl \prop_get:NVNTF \g__kanban_palette_texts_prop \l__kanban_palette_name_tl \l__kanban_palette_fg_tl {\clist_set:NV \l__kanban_board_palette_fg_clist \l__kanban_palette_fg_tl} {\clist_clear:N \l__kanban_board_palette_fg_clist} } {\use:x {\msg_warning:nnnn {kanban-board}{unknown-palette} {board~palette}{\exp_not:V \l__kanban_palette_name_tl}}} } \cs_new_protected:Npn \kanban@paletteboardinit { \clist_set_eq:NN \l__kanban_board_palette_clist \g__kanban_palette_clist \clist_set_eq:NN \l__kanban_board_palette_fg_clist \g__kanban_palette_fg_clist } % \end{macrocode} % % \paragraph{Cyclic palette lookup.} % A board may contain more columns than its palette contains colors. The % lookup therefore maps each one-based column number cyclically onto the % board palette and publishes both the background and its heading foreground % through two LaTeX2e-facing macros. If the selected palette has no explicit % foreground list, the heading color of the board theme is used. % \begin{macrocode} \cs_new_protected:Npn \kanban@palettecolor #1 { \int_set:Nn \l__kanban_palette_count_int {\clist_count:N \l__kanban_board_palette_clist} \int_set:Nn \l__kanban_palette_index_int {1 + \int_mod:nn {#1 - 1} {\l__kanban_palette_count_int}} \tl_set:Nx \l__kanban_palette_item_tl { \clist_item:Nn \l__kanban_board_palette_clist {\l__kanban_palette_index_int} } \cs_set:Npx \kb@thiscolor {\tl_use:N \l__kanban_palette_item_tl} \clist_if_empty:NTF \l__kanban_board_palette_fg_clist {\cs_set:Npn \kb@thisheadingtext {\kanban@headingtext}} { \tl_set:Nx \l__kanban_palette_item_tl { \clist_item:Nn \l__kanban_board_palette_fg_clist {\l__kanban_palette_index_int} } \cs_set:Npx \kb@thisheadingtext {\tl_use:N \l__kanban_palette_item_tl} } } % \end{macrocode} % % \begin{macro}{\kanbandefinepalette} % \begin{macro}{\kbdefinepalette} % The public definition command exposes both registry formats through one % interface. The unstarred form accepts a list of backgrounds and the starred % form accepts background/foreground pairs. The long command is the stable % public name. The concise alias is installed only when that name is free, so % loading \textsf{kanban-board} never overwrites a command owned by another % package. % \begin{macrocode} \NewDocumentCommand \kanbandefinepalette {smm} { \IfBooleanTF{#1} {\__kanban_palette_define_pairs:nn {#2}{#3}} {\__kanban_palette_define:nn {#2}{#3}} } \cs_if_exist:NTF \kbdefinepalette {\msg_warning:nnnn {kanban-board}{alias-taken} {kbdefinepalette}{kanbandefinepalette}} {\cs_new_eq:NN \kbdefinepalette \kanbandefinepalette} % \end{macrocode} % \end{macro} % \end{macro} % % \paragraph{Internal selection bridges.} % The setup and board-key layers are written partly in LaTeX2e syntax. These % wrappers give those layers stable names for the selection functions and % add no state or validation of their own. % \begin{macrocode} \cs_new_protected:Npn \kanban@selectpalette #1 {\__kanban_palette_select_global:n {#1}} \cs_new_protected:Npn \kanban@selectboardpalette #1 {\__kanban_palette_select_board:n {#1}} % \end{macrocode} % % \paragraph{Column-name normalization.} % Column identity is case-insensitive and ignores formatting, but the % visible heading keeps the spelling given in the column list. The % single-name function therefore produces two results. It stores the trimmed % input as the display text and writes the lookup key into the destination % variable. The key is the purified text of the name in uppercase. Ordinary % macros are expanded, and formatting commands and braces are removed. A % column declared as |\emph{Soon}| is therefore addressed as |soon| by a % task, and the heading still shows the emphasized text. Purification also % keeps robust commands out of the expanded definitions that later publish % the key list. The list function applies that rule to every item, ignores % blank items, records the first spelling of each key in a display map, and % diagnoses duplicates after normalization. Membership is tested in the % display map rather than in the key list, because a property list treats % every key as a string, whereas the comma list test does not support items % that contain a brace group. A duplicate is a user error rather than a % recoverable preference, so it produces an error with corrective help, and % the first occurrence is retained. % \begin{macrocode} \cs_new_protected:Npn \__kanban_column_normalize:nN #1#2 { \tl_set:Nn \l__kanban_column_display_tl {#1} \tl_trim_spaces:N \l__kanban_column_display_tl \tl_set:Nx #2 {\exp_args:NV \text_purify:n \l__kanban_column_display_tl} \tl_set:Nx #2 {\exp_args:NV \text_uppercase:n #2} \tl_trim_spaces:N #2 } \cs_new_protected:Npn \__kanban_columns_normalize:nNN #1#2#3 { \clist_clear:N #2 \prop_clear:N #3 \clist_map_inline:nn {#1} { \__kanban_column_normalize:nN {##1} \l__kanban_column_tl \tl_if_blank:VF \l__kanban_column_tl { \prop_if_in:NVTF #3 \l__kanban_column_tl {\PackageError{kanban-board} {duplicate~kanban~column~ '\tl_use:N \l__kanban_column_tl'} {Column~names~are~case-insensitive.\MessageBreak Remove~the~repeated~name~from~the~column~list.}} { \clist_put_right:NV #2 \l__kanban_column_tl \prop_put:NVV #3 \l__kanban_column_tl \l__kanban_column_display_tl } } } } % \end{macrocode} % % \paragraph{Persistent and board-local column lists.} % As with palettes, columns have a persistent default and a board-local % snapshot. Each consists of the normalized key list and the display map. % Document setup accepts only a non-empty normalized list and makes both % parts global. Board initialization copies the defaults locally. A non-blank % board override replaces the copy without affecting later boards. % \begin{macrocode} \cs_new_protected:Npn \__kanban_columns_select_global:n #1 { \__kanban_columns_normalize:nNN {#1} \l__kanban_columns_tmp_clist \l__kanban_column_display_tmp_prop \clist_if_empty:NTF \l__kanban_columns_tmp_clist {\msg_warning:nn {kanban-board}{empty-column-list}} { \clist_gset_eq:NN \g__kanban_columns_clist \l__kanban_columns_tmp_clist \prop_gset_eq:NN \g__kanban_column_display_prop \l__kanban_column_display_tmp_prop } } \cs_new_protected:Npn \kanban@selectcolumns #1 {\__kanban_columns_select_global:n {#1}} \cs_new_protected:Npn \kanban@columnsboardinit { \clist_set_eq:NN \l__kanban_board_columns_clist \g__kanban_columns_clist \prop_set_eq:NN \l__kanban_board_column_display_prop \g__kanban_column_display_prop } \cs_new_protected:Npn \kanban@selectboardcolumns #1 { \tl_if_blank:nF {#1} { \__kanban_columns_normalize:nNN {#1} \l__kanban_columns_tmp_clist \l__kanban_column_display_tmp_prop \clist_if_empty:NF \l__kanban_columns_tmp_clist { \clist_set_eq:NN \l__kanban_board_columns_clist \l__kanban_columns_tmp_clist \prop_set_eq:NN \l__kanban_board_column_display_prop \l__kanban_column_display_tmp_prop } } } % \end{macrocode} % % \paragraph{Task-target resolution.} % A task names a column textually, but collection must append the task to a % numeric board-local list. The resolver normalizes the supplied name, % exposes the normalized text for diagnostics, and looks it up in the % name-to-identifier map of the active board. A missing key leaves the % identifier empty, which lets the collection layer issue the % context-specific error. % \begin{macrocode} \cs_new_protected:Npn \kanban@normtaskcol #1 { \__kanban_column_normalize:nN {#1} \l__kanban_column_tl \cs_set:Npx \kbtask@column {\tl_use:N \l__kanban_column_tl} \cs_set:Npn \kbtask@columnid { } \prop_get:NVNTF \l__kanban_column_ids_prop \l__kanban_column_tl \l__kanban_column_id_tl {\cs_set:Npx \kbtask@columnid {\tl_use:N \l__kanban_column_id_tl}} { } } % \end{macrocode} % % \paragraph{Numeric column registration.} % Before tasks can be collected, every normalized board column receives a % consecutive identifier. The setup function also prepares the printable list % of valid keys for error messages and delegates the creation of per-column % counters, lists, colors, and renderer entries to |\kanban@setupcolumn|, % which is defined in the board part. The printable list is expanded through % |\text_expand:n|, which expands the comma list but leaves any robust % command intact, so the expanded definition cannot be derailed. The display % text is passed by value so that it is stored unexpanded for the heading. % \begin{macrocode} \cs_new_protected:Npn \kanban@setupboardcolumns { \cs_set:Npx \kanban@availablecolumns { \text_expand:n {\clist_use:Nn \l__kanban_board_columns_clist {,}} } \prop_clear:N \l__kanban_column_ids_prop \int_zero:N \l__kanban_column_index_int \clist_map_inline:Nn \l__kanban_board_columns_clist { \int_incr:N \l__kanban_column_index_int \tl_set:Nx \l__kanban_column_id_tl {\int_use:N \l__kanban_column_index_int} \prop_put:NnV \l__kanban_column_ids_prop {##1} \l__kanban_column_id_tl \prop_get:NnN \l__kanban_board_column_display_prop {##1} \l__kanban_column_display_tl \exp_args:NVV \kanban@setupcolumn \l__kanban_column_display_tl \l__kanban_column_id_tl } } % \end{macrocode} % % \paragraph{Choice normalization and registry cleanup.} % Several key values, including modes and date-format names, share the same % case-insensitive choice syntax. The choice helper trims and lowercases such % a value before publishing it as |\kb@choice|. The lowercase conversion % expands ordinary macros and leaves robust commands intact, and the % published copy is not expanded a second time. A robust command in a choice % is therefore reported as an unknown value instead of derailing the % definition. At the end of the board % lifecycle, the cleanup function removes all dynamically created per-column % control sequences so that completed boards do not retain card material or % counters in the hash table. The names are constructed from character data, % so the |c| argument type is required. % \begin{macrocode} \cs_new_protected:Npn \kanban@normalizechoice #1 { \tl_set:Nn \l__kanban_choice_tl {#1} \tl_trim_spaces:N \l__kanban_choice_tl \tl_set:Nx \l__kanban_choice_tl {\exp_args:NV \text_lowercase:n \l__kanban_choice_tl} \cs_set:Npx \kb@choice {\exp_not:V \l__kanban_choice_tl} } \cs_new_protected:Npn \kanban@cleanupboard { \int_step_inline:nn {\kanban@ncols} { \cs_undefine:c {kanban@name@##1} \cs_undefine:c {kanban@color@##1} \cs_undefine:c {kanban@headingtext@##1} \cs_undefine:c {kanban@list@\kanban@boardgroup @##1} \cs_undefine:c {kanban@count@\kanban@boardgroup @##1} } } % \end{macrocode} % % \begin{macrocode} % % \end{macrocode}