% \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{Task collection} % % The task key family stores metadata without expanding the card text. The % key name of an unknown option is passed by expansion, because % \textsf{l3msg} shows message arguments without expanding them. A % valid declaration appends one protected renderer call to the selected % board-local list. Invalid or missing targets are diagnosed and never affect % the counter. Collection and rendering are separate phases. This permits the % environment to determine the tallest heading, the highest column, and the % final column counts before any task card is typeset. % % \paragraph{Task-option keys.} % The task family translates user-facing metadata into temporary task % macros. Priority and background use validated setters. Author and deadline % keep their token lists. The |due| key aliases |deadline| and |background| % aliases |bg|. The |late| and |completed| keys are valueless flags, so a % misleading form such as |completed=false| is rejected. An unknown task % option is ignored with a scoped warning. % \begin{macrocode} \ExplSyntaxOn \keys_define:nn {kanban/task} { prio .code:n = {\kanban@setpriority{#1}}, author .code:n = {\def\kbtask@author{#1}}, deadline .code:n = {\def\kbtask@deadline{#1}}, due .meta:n = {deadline={#1}}, late .code:n = {\def\kbtask@late{1}}, late .value_forbidden:n = true, completed .code:n = {\def\kbtask@completed{1}}, completed .value_forbidden:n = true, bg .code:n = {\kanban@setbackground{#1}}, background .meta:n = {bg={#1}}, unknown .code:n = {\use:x {\msg_warning:nnnn {kanban-board}{unknown-key} {task}{\l_keys_key_str}}} } \cs_new_protected:Npn \kanban@processtaskoptions #1 {\keys_set:nn {kanban/task}{#1}} \ExplSyntaxOff % \end{macrocode} % % \paragraph{Validation and deferred task collection.} % The collector first gives every metadata field an empty value and % processes the option list. It then requires an active board, a non-empty % normalized target, and a target present in the identifier map of the % board. Each failed condition emits a specific error and ignores the task. % A task inside a rejected nested board is discarded silently because the % nesting error has already explained the cause. A task outside any board is % diagnosed on its own. % % For a valid target, the counter is incremented globally and a % self-contained eight-argument call to |\kanban@rendercard| is appended % globally to the task list of the column. The renderer command is kept % unexpanded for the rendering phase, the column color is resolved at once % because it is already fixed, and the task body and all metadata are % protected from expansion. The deadline is then checked once, so that any % warning names the line of the declaration. % \begin{macrocode} \newcommand\kanban@dotask[3][]{% \def\kbtask@prio{}\def\kbtask@author{}\def\kbtask@deadline{}% \def\kbtask@late{}\def\kbtask@completed{}\def\kbtask@bg{}% \kanban@processtaskoptions{#1}% \ifbool{kanban@active} {\kanban@normtaskcol{#2}% \ifdefempty\kbtask@column {\PackageError{kanban-board} {task has no assigned column. \MessageBreak Valid columns: \kanban@availablecolumns} {The first mandatory argument of \string\kanbantask\space must name a column from the enclosing board.\MessageBreak The task was ignored.}} {\ifdefempty\kbtask@columnid {\PackageError{kanban-board} {unknown kanban column '#2'. \MessageBreak Valid columns: \kanban@availablecolumns} {Column names are matched without regard to letter case. \MessageBreak The task was ignored because its normalized target is not in this board.}} {\expandafter\numgdef \csname kanban@count@\kanban@boardgroup @\kbtask@columnid\endcsname {\csname kanban@count@\kanban@boardgroup @\kbtask@columnid\endcsname+1}% \csxappto{kanban@list@\kanban@boardgroup @\kbtask@columnid}{% \noexpand\kanban@rendercard {\csuse{kanban@color@\kbtask@columnid}}{\unexpanded{#3}}% {\unexpanded\expandafter{\kbtask@prio}}% {\unexpanded\expandafter{\kbtask@author}}% {\unexpanded\expandafter{\kbtask@deadline}}% {\unexpanded\expandafter{\kbtask@late}}% {\unexpanded\expandafter{\kbtask@completed}}% {\unexpanded\expandafter{\kbtask@bg}}}% \expandafter\kanban@diagnosedeadline\expandafter{% \kbtask@deadline}% }}} {\ifbool{kanban@discardtasks}{} {\PackageError{kanban-board} {task declared outside a kanban environment} {Move the task declaration between \string\begin{kanban} and \string\end{kanban}. The task was ignored.}}}} % \end{macrocode} % % \begin{macro}{\kanbantask} % \begin{macro}{\kbtask} % The canonical public command supplies the compact task declaration syntax % and delegates all key processing, validation, and collection to the % internal implementation. The text argument is long, so a card may contain % several paragraphs. The option list and the column name remain short, % because a paragraph break inside them indicates a missing brace, and a % short argument turns that mistake into an immediate error instead of % swallowing the rest of the document. The concise command is installed by % equivalence only when its name is free. An existing definition is % preserved with a warning. % \begin{macrocode} \NewDocumentCommand\kanbantask{O{}m+m}{% \kanban@dotask[#1]{#2}{#3}} \@ifundefined{kbtask} {\let\kbtask\kanbantask} {\kanban@warning{alias-taken}{kbtask}{kanbantask}} % \end{macrocode} % \end{macro} % \end{macro} % % \subsection{Board lifecycle} % % Board keys update only the active board snapshot. Column setup allocates % the isolated lists, prepares both rendering sequences, and validates the % complete gap width before typesetting begins. The entry routine rejects % nesting before any outer state can be replaced. The environment supplies a % TeX group, so assignments divided into persistent and local forms acquire % the documented lifetime without a separate restoration procedure. % % \paragraph{Board column selection and board-option keys.} % A blank board column value means ``use the persistent default''. A % non-blank value is normalized as a board-local override. The board key % family provides local counterparts to the persistent setup keys and adds % the valueless |break| switch. Start and reference dates use the same % deferred strategy as persistent setup, so key order cannot alter % ambiguous-date parsing. % \begin{macrocode} \newcommand\kanban@setboardcolumns[1]{% \ifblank{#1} {\kanban@columnsboardinit} {\kanban@selectboardcolumns{#1}}} \ExplSyntaxOn \keys_define:nn {kanban/board} { break .code:n = {\booltrue{kanban@break}}, break .value_forbidden:n = true, columns .code:n = {\kanban@setboardcolumns{#1}}, date-format .code:n = {\kanban@selectboarddateformat{#1}}, dateformat .meta:n = {date-format={#1}}, startdate .code:n = { \tl_set:Nn \l__kanban_pending_startdate_tl {#1} \bool_set_true:N \l__kanban_pending_startdate_bool }, reference-date .code:n = { \tl_set:Nn \l__kanban_pending_reference_date_tl {#1} \bool_set_true:N \l__kanban_pending_reference_date_bool }, today .meta:n = {reference-date={#1}}, palette .code:n = {\kanban@selectboardpalette{#1}}, mode .code:n = {\kanban@selectboardmode{#1}}, columnsep .code:n = {\kanban@setboardcolumnsep{#1}}, labels .code:n = {\keys_set:nn {kanban/board-labels}{#1}}, unknown .code:n = {\use:x {\msg_warning:nnnn {kanban-board}{unknown-key} {board}{\l_keys_key_str}}} } % \end{macrocode} % % \paragraph{A deferred board-option transaction.} % Board-option processing mirrors persistent setup but commits staged dates % to the local snapshot setters. All immediate options, including a possible % |date-format| override, are resolved before the dates are parsed. The % transaction runs after the persistent values have been copied into the % board, so omitted keys inherit their defaults. % \begin{macrocode} \cs_new_protected:Npn \kanban@processboardoptions #1 { \bool_set_false:N \l__kanban_pending_startdate_bool \bool_set_false:N \l__kanban_pending_reference_date_bool \keys_set:nn {kanban/board}{#1} \bool_if:NT \l__kanban_pending_startdate_bool {\exp_args:NV \kanban@setboardstartdate \l__kanban_pending_startdate_tl} \bool_if:NT \l__kanban_pending_reference_date_bool {\exp_args:NV \kanban@setboardreferencedate \l__kanban_pending_reference_date_tl} } \ExplSyntaxOff % \end{macrocode} % % \paragraph{Per-column state and renderer-sequence construction.} % Each normalized column is represented internally by a numeric identifier. % Setup stores the display name, initializes the board-qualified deferred % task list and counter, assigns the cyclic palette background and % foreground, and appends one adapter call to each renderer sequence. Card % lists and counts carry the board number because those definitions are % global during collection. Later columns in the break sequence are preceded % by |\switchcolumn| so that |paracol| receives one stream per board column. % \begin{macrocode} \newcommand\kanban@setupcolumn[2]{% \csdef{kanban@name@#2}{#1}% \csgdef{kanban@list@\kanban@boardgroup @#2}{}% \csgdef{kanban@count@\kanban@boardgroup @#2}{0}% \kanban@palettecolor{#2}\csedef{kanban@color@#2}{\kb@thiscolor}% \csedef{kanban@headingtext@#2}{\kb@thisheadingtext}% \numdef\kanban@ncols{\kanban@ncols+1}% \eappto\kanban@headseq{\noexpand\kanban@onehead{#2}}% \eappto\kanban@colseq{\noexpand\kanban@onecol{#2}}% \ifnumequal{#2}{1} {\eappto\kanban@colseq@b{\noexpand\kanban@onecol@b{#2}}} {\eappto\kanban@colseq@b{\noexpand\switchcolumn \noexpand\kanban@onecol@b{#2}}}} % \end{macrocode} % % \paragraph{Whole-board geometry validation.} % A syntactically valid non-negative gap may still be impossible for a board % with many columns. Before rendering, the validator computes the total width % of the $N-1$ inter-column gaps and requires that total to be strictly % smaller than |\linewidth|. An impossible value produces an actionable error % and falls back to zero for this board, which lets the document continue. % \begin{macrocode} \newcommand\kanban@validateboardgeometry{% \ifnum\kanban@ncols>1\relax \kanban@totalgaps=\kanban@boardcolumnsep \multiply\kanban@totalgaps by \numexpr\kanban@ncols-1\relax \ifdim\kanban@totalgaps<\linewidth\relax\else \PackageError{kanban-board} {column separation leaves no width for columns} {For N columns, (N-1) times columnsep must be smaller than \string\linewidth.\MessageBreak The current board will use columnsep=0pt.}% \kanban@boardcolumnsep=0pt \fi \fi} % \end{macrocode} % % \paragraph{Environment entry and state snapshot.} % Opening a board either rejects unsupported nesting or creates a complete % isolated snapshot before tasks are read. A rejected inner environment % clears its render and active flags locally, sets the discard flag, and % issues one error, so its body can be read safely without being attached % to the outer board. The successful path resets the lifecycle flags, % advances the unique board identifier, copies every persistent scalar and % list default, processes local overrides, materializes the visual mode, % clears the renderer sequences, registers all columns, validates the % aggregate geometry, and suppresses source whitespace before the first % task. % \begin{macrocode} \newcommand\kanban@begin[1]{% \ifbool{kanban@active} {\boolfalse{kanban@render}% \boolfalse{kanban@active}% \booltrue{kanban@discardtasks}% \PackageError{kanban-board} {nested kanban environments are not supported} {Close the current board before opening another one. \MessageBreak The nested board was ignored.}} {\booltrue{kanban@render}% \boolfalse{kanban@break}% \boolfalse{kanban@discardtasks}% \booltrue{kanban@active}% \stepcounter{kanban@board}% \edef\kanban@boardgroup{\arabic{kanban@board}}% \edef\kanban@thestart{\kanban@startdate}% \edef\kanban@boarddateformat{\kanban@dateformat}% \edef\kanban@thereference{\kanban@referencedate}% \edef\kanban@boardmode{\kanban@mode}% \let\kanban@boardhighlabel\kanban@highlabel \let\kanban@boardmediumlabel\kanban@mediumlabel \let\kanban@boardlowlabel\kanban@lowlabel \let\kanban@boarddonelabel\kanban@donelabel \kanban@boardcolumnsep=\kanban@columnsep \kanban@paletteboardinit \kanban@columnsboardinit \kanban@processboardoptions{#1}% \kanban@applymode \def\kanban@ncols{0}\def\kanban@headseq{}% \def\kanban@colseq{}\def\kanban@colseq@b{}% \kanban@setupboardcolumns \kanban@validateboardgeometry \ignorespaces}} % \end{macrocode} % % \begin{macro}{\kanban} % \begin{macro}{\endkanban} % The public environment has one optional board-key list and no mandatory % column argument. Columns come from the persistent default unless a % board-local |columns| key overrides them. Closing the environment selects % the page-breakable |paracol| renderer or the indivisible fixed raster. A % rejected nested environment has its render flag cleared and therefore % produces no board. Successfully rendered boards release their dynamically % named state afterward, and the environment group restores all remaining % local snapshots. % % In break mode, the board gap is assigned to the standard |\columnsep| used % by |paracol|, and the prepared sequence supplies headers, cards, and % |\switchcolumn| calls. In fixed mode, a full-line |minipage| makes the % board indivisible. The raster has one column per Kanban stage and two rows, % all headers followed by all bodies, with equal heights within each row. % \begin{macrocode} \NewDocumentEnvironment{kanban}{O{}} {\kanban@begin{#1}} {\ifbool{kanban@render}{\par\medskip \ifbool{kanban@break} {\setlength\columnsep{\kanban@boardcolumnsep}% \begin{paracol}{\kanban@ncols}\kanban@colseq@b\end{paracol}} {\noindent\begin{minipage}{\linewidth}% \begin{tcbraster}[ raster columns=\kanban@ncols, raster rows=2, raster width=\linewidth, raster column skip=\kanban@boardcolumnsep, raster row skip=0pt, raster before skip=0pt, raster after skip=0pt, raster equal height=rows, raster valign=top] \kanban@headseq\kanban@colseq \end{tcbraster}% \end{minipage}}% \par\vspace{2mm}\kanban@cleanupboard}{}} % % \end{macrocode} % \end{macro} % \end{macro} % \Finale