% \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{Deadline engine}
%
% Deadline classification uses anchored regular expressions before any
% numeric conversion. The five supported notations enter one component
% validator and one formatter. Bounded project-month labels use a separate
% validated path, and arbitrary descriptive labels remain neutral and
% unchanged. The engine separates syntax recognition, Gregorian validation,
% display formatting, and temporal classification. This separation lets a
% task keep a neutral descriptive deadline without misclassifying it as a
% broken date. Diagnostics are issued when a task is declared, so a warning
% names the line of the task rather than the end of the board.
%
% \paragraph{Typed parsing and calendar state.}
% Date processing needs the normalized input, the capture groups of the last
% match, numeric components, maximum-day calculations, project-month state,
% reference-date state, display fragments, and explicit status flags. Each
% role has its own variable, so no scratch register carries two meanings
% across nested tests. The scratch variables of the kernel are deliberately
% not used, because other packages may overwrite them between calls.
%    \begin{macrocode}
\tl_new:N \l__kanban_deadline_tl
\seq_new:N \l__kanban_deadline_match_seq
\int_new:N \l__kanban_deadline_day_int
\int_new:N \l__kanban_deadline_month_int
\int_new:N \l__kanban_deadline_year_int
\int_new:N \l__kanban_deadline_max_day_int
\int_new:N \l__kanban_swap_int
\int_new:N \l__kanban_deadline_key_int
\int_new:N \l__kanban_reference_key_int
\int_new:N \l__kanban_project_month_int
\int_new:N \l__kanban_current_month_int
\int_new:N \l__kanban_project_start_day_int
\int_new:N \l__kanban_project_start_month_int
\int_new:N \l__kanban_project_start_year_int
\int_new:N \l__kanban_project_boundary_day_int
\int_new:N \l__kanban_reference_day_int
\int_new:N \l__kanban_reference_month_int
\int_new:N \l__kanban_reference_year_int
\tl_new:N \l__kanban_reference_tl
\tl_new:N \l__kanban_display_day_tl
\tl_new:N \l__kanban_display_month_tl
\tl_new:N \l__kanban_display_year_tl
\tl_new:N \l__kanban_display_format_tl
\tl_new:N \l__kanban_dimension_tl
\bool_new:N \l__kanban_valid_date_bool
\bool_new:N \l__kanban_valid_project_month_bool
\bool_new:N \l__kanban_date_syntax_bool
\bool_new:N \l__kanban_date_components_bool
\bool_new:N \l__kanban_ambiguous_slash_bool
\bool_new:N \l__kanban_project_month_syntax_bool
%    \end{macrocode}
%
% The two syntax flags have distinct meanings. The syntax flag records that
% the input has the shape of a date, which is the condition for a warning.
% The components flag records that one of the exact notations captured year,
% month, and day, which is the condition for validation.
%
% \paragraph{Maximum days in a Gregorian month.}
% Component validation needs the number of permitted days in the parsed
% month. February has 29 days in years divisible by 400, or divisible by 4
% but not by 100. April, June, September, and November have 30 days. All
% other valid months have 31. The divisibility predicate reads the year
% component that the parser has already assigned.
%    \begin{macrocode}
\prg_new_conditional:Npnn \__kanban_year_divisible:n #1 {p}
  {
    \int_compare:nNnTF
      {\int_mod:nn {\l__kanban_deadline_year_int} {#1}} = {0}
      {\prg_return_true:}
      {\prg_return_false:}
  }
\cs_new_protected:Npn \__kanban_set_max_day:
  {
    \int_case:nnF {\l__kanban_deadline_month_int}
      {
        {2}
          {
            \bool_if:nTF
              {
                \__kanban_year_divisible_p:n {400}
                || (
                     \__kanban_year_divisible_p:n {4}
                     && ! \__kanban_year_divisible_p:n {100}
                   )
              }
              {\int_set:Nn \l__kanban_deadline_max_day_int {29}}
              {\int_set:Nn \l__kanban_deadline_max_day_int {28}}
          }
        {4}{\int_set:Nn \l__kanban_deadline_max_day_int {30}}
        {6}{\int_set:Nn \l__kanban_deadline_max_day_int {30}}
        {9}{\int_set:Nn \l__kanban_deadline_max_day_int {30}}
        {11}{\int_set:Nn \l__kanban_deadline_max_day_int {30}}
      }
      {\int_set:Nn \l__kanban_deadline_max_day_int {31}}
  }
%    \end{macrocode}
%
% \paragraph{Gregorian component validation and capture conversion.}
% Once a notation has assigned year, month, and day, the validator checks
% that the year is positive, that the month lies between 1 and 12, and that
% the day lies in the range permitted by the month and the leap-year rule.
% The tests are nested so that the maximum-day helper never sees an invalid
% month. The capture helpers provide the single conversion path from
% regular-expression text to integers. The three-argument helper receives the
% capture positions of year, month, and day and also records that the input
% had both date syntax and complete components.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_validate_date:
  {
    \bool_set_false:N \l__kanban_valid_date_bool
    \int_compare:nNnT {\l__kanban_deadline_year_int} > {0}
      {
        \int_compare:nNnT {\l__kanban_deadline_month_int} > {0}
          {
            \int_compare:nNnT {\l__kanban_deadline_month_int} < {13}
              {
                \__kanban_set_max_day:
                \int_compare:nNnT {\l__kanban_deadline_day_int} > {0}
                  {
                    \int_compare:nNnT
                      {\l__kanban_deadline_day_int}
                      < {\l__kanban_deadline_max_day_int + 1}
                      {\bool_set_true:N \l__kanban_valid_date_bool}
                  }
              }
          }
      }
  }
\cs_new_protected:Npn \__kanban_set_match:Nn #1#2
  {
    \int_set:Nn #1
      {\seq_item:Nn \l__kanban_deadline_match_seq {#2}}
  }
\cs_new_protected:Npn \__kanban_set_components:nnn #1#2#3
  {
    \bool_set_true:N \l__kanban_date_syntax_bool
    \bool_set_true:N \l__kanban_date_components_bool
    \__kanban_set_match:Nn \l__kanban_deadline_year_int {#1}
    \__kanban_set_match:Nn \l__kanban_deadline_month_int {#2}
    \__kanban_set_match:Nn \l__kanban_deadline_day_int {#3}
  }
%    \end{macrocode}
%
% \paragraph{Recognition of the five date notations.}
% The parser recognizes dotted day-first dates, hyphenated ISO dates,
% slash-separated year-first dates, and the two locale-dependent slash forms.
% Every notation requires a four-digit year. Month and day may have one or
% two digits. For an ambiguous year-last slash date, the selected date format
% determines the first interpretation. If that interpretation is impossible,
% the parser swaps day and month once and validates the alternative, which
% accepts unambiguous input such as |31/12/2026| under any format.
%
% Input that has the shape of a date but matches none of the exact notations,
% for example a two-digit year, sets only the syntax flag. Such input is
% reported as an invalid date and is never silently treated as a label. This
% protects against |18.07.26| being read as a date in the year 26.
%
% The input is expanded through |\text_expand:n| before matching. Ordinary
% macros are expanded to their text, so a macro that holds a date is
% classified. Robust and protected commands are left intact, so a deadline
% such as |\emph{soon}| is a neutral label and cannot derail the expanded
% assignment.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_parse_date:nn #1#2
  {
    \bool_set_false:N \l__kanban_valid_date_bool
    \bool_set_false:N \l__kanban_date_syntax_bool
    \bool_set_false:N \l__kanban_date_components_bool
    \bool_set_false:N \l__kanban_ambiguous_slash_bool
    \tl_set:Nx \l__kanban_deadline_tl {\text_expand:n {#1}}
    \tl_trim_spaces:N \l__kanban_deadline_tl
    \exp_args:NnV \regex_extract_once:nnNTF
      {\A([0-9]{1,2})\.([0-9]{1,2})\.([0-9]{4})\Z}
      \l__kanban_deadline_tl
      \l__kanban_deadline_match_seq
      {\__kanban_set_components:nnn {4}{3}{2}}
      {
        \exp_args:NnV \regex_extract_once:nnNTF
          {\A([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})\Z}
          \l__kanban_deadline_tl
          \l__kanban_deadline_match_seq
          {\__kanban_set_components:nnn {2}{3}{4}}
          {
            \exp_args:NnV \regex_extract_once:nnNTF
              {\A([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})\Z}
              \l__kanban_deadline_tl
              \l__kanban_deadline_match_seq
              {\__kanban_set_components:nnn {2}{3}{4}}
              {
                \exp_args:NnV \regex_extract_once:nnNTF
                  {\A([0-9]{1,2})/([0-9]{1,2})/([0-9]{4})\Z}
                  \l__kanban_deadline_tl
                  \l__kanban_deadline_match_seq
                  {
                    \bool_set_true:N \l__kanban_ambiguous_slash_bool
                    \bool_if:nTF
                      {
                        \str_if_eq_p:ee {#2} {english}
                        || \str_if_eq_p:ee {#2} {german}
                      }
                      {\__kanban_set_components:nnn {4}{3}{2}}
                      {\__kanban_set_components:nnn {4}{2}{3}}
                  }
                  {
                    \exp_args:NnV \regex_match:nnT
                      {\A[0-9]{1,4}[.\-/][0-9]{1,2}[.\-/][0-9]{1,4}\Z}
                      \l__kanban_deadline_tl
                      {\bool_set_true:N \l__kanban_date_syntax_bool}
                  }
              }
          }
      }
    \bool_if:NT \l__kanban_date_components_bool
      {\__kanban_validate_date:}
    \bool_if:nT
      {
        \l__kanban_ambiguous_slash_bool
        && ! \l__kanban_valid_date_bool
      }
      {
        \int_set_eq:NN \l__kanban_swap_int \l__kanban_deadline_day_int
        \int_set_eq:NN \l__kanban_deadline_day_int
          \l__kanban_deadline_month_int
        \int_set_eq:NN \l__kanban_deadline_month_int \l__kanban_swap_int
        \__kanban_validate_date:
      }
  }
%    \end{macrocode}
%
% \paragraph{Canonical and display formatting.}
% Accepted dates are stored internally in one unambiguous year, month, and
% day representation. At rendering time the same numeric components are
% formatted according to the notation selected for the board. Keeping
% canonical storage and presentation separate ensures that |date-format|
% affects input interpretation and display but never chronological
% comparison. The padding helpers are expandable so that the canonical form
% can be produced inside an expanded definition. The display macro keeps the
% literal deadline for invalid dates and descriptive labels, so those values
% remain printable.
%    \begin{macrocode}
\cs_new:Npn \__kanban_two_digits:n #1
  {
    \int_compare:nNnT {#1} < {10} {0}
    \int_to_arabic:n {#1}
  }
\cs_new:Npn \__kanban_four_digits:n #1
  {
    \int_compare:nNnT {#1} < {1000} {0}
    \int_compare:nNnT {#1} < {100} {0}
    \int_compare:nNnT {#1} < {10} {0}
    \int_to_arabic:n {#1}
  }
\cs_new:Npn \__kanban_canonical_date:
  {
    \__kanban_four_digits:n {\l__kanban_deadline_year_int}
    -\__kanban_two_digits:n {\l__kanban_deadline_month_int}
    -\__kanban_two_digits:n {\l__kanban_deadline_day_int}
  }
\cs_new_protected:Npn \kanban@preparedeadline #1
  {
    \cs_set:Npn \kb@displaydeadline {#1}
    \__kanban_parse_date:nn {#1} {\kanban@boarddateformat}
    \bool_if:NT \l__kanban_valid_date_bool
      {
        \tl_set:Nx \l__kanban_display_day_tl
          {\__kanban_two_digits:n {\l__kanban_deadline_day_int}}
        \tl_set:Nx \l__kanban_display_month_tl
          {\__kanban_two_digits:n {\l__kanban_deadline_month_int}}
        \tl_set:Nx \l__kanban_display_year_tl
          {\__kanban_four_digits:n {\l__kanban_deadline_year_int}}
        \tl_set:Nx \l__kanban_display_format_tl
          {\kanban@boarddateformat}
        \str_case:VnF \l__kanban_display_format_tl
          {
            {us}
              {
                \cs_set:Npx \kb@displaydeadline
                  {
                    \l__kanban_display_month_tl /
                    \l__kanban_display_day_tl /
                    \l__kanban_display_year_tl
                  }
              }
            {english}
              {
                \cs_set:Npx \kb@displaydeadline
                  {
                    \l__kanban_display_day_tl /
                    \l__kanban_display_month_tl /
                    \l__kanban_display_year_tl
                  }
              }
            {german}
              {
                \cs_set:Npx \kb@displaydeadline
                  {
                    \l__kanban_display_day_tl .
                    \l__kanban_display_month_tl .
                    \l__kanban_display_year_tl
                  }
              }
            {iso}
              {
                \cs_set:Npx \kb@displaydeadline
                  {
                    \l__kanban_display_year_tl -
                    \l__kanban_display_month_tl -
                    \l__kanban_display_day_tl
                  }
              }
            {ymd}
              {
                \cs_set:Npx \kb@displaydeadline
                  {
                    \l__kanban_display_year_tl /
                    \l__kanban_display_month_tl /
                    \l__kanban_display_day_tl
                  }
              }
          }
          { }
      }
  }
%    \end{macrocode}
%
% \paragraph{Project-month syntax.}
% Project deadlines such as |M1| are relative to the configured start date
% rather than to a calendar date written on the card. The parser accepts a
% positive integer of at most six significant digits and allows leading
% zeros, so |M01| denotes the first month while |M0| and |M000| remain
% invalid. It records recognized but invalid |M|-prefixed numeric syntax
% separately and leaves all other text neutral. The bound keeps malformed
% input away from integer arithmetic while remaining far beyond any
% realistic project duration. The input is expanded through
% |\text_expand:n| in the same way as a calendar date.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_parse_project_month:n #1
  {
    \bool_set_false:N \l__kanban_valid_project_month_bool
    \bool_set_false:N \l__kanban_project_month_syntax_bool
    \tl_set:Nx \l__kanban_deadline_tl {\text_expand:n {#1}}
    \tl_trim_spaces:N \l__kanban_deadline_tl
    \exp_args:NnV \regex_match:nnT
      {\AM[0-9]+\Z}
      \l__kanban_deadline_tl
      {\bool_set_true:N \l__kanban_project_month_syntax_bool}
    \exp_args:NnV \regex_extract_once:nnNTF
      {\AM0*([1-9][0-9]{0,5})\Z}
      \l__kanban_deadline_tl
      \l__kanban_deadline_match_seq
      {
        \int_set:Nn \l__kanban_project_month_int
          {\seq_item:Nn \l__kanban_deadline_match_seq {2}}
        \bool_set_true:N \l__kanban_valid_project_month_bool
      }
      { }
  }
%    \end{macrocode}
%
% \paragraph{Reference-date loading and calendar classification.}
% Classification is evaluated against the board-local reference date. If
% none was configured, the engine reads the compilation year, month, and day
% from TeX. A configured value is already canonical, so one exact expression
% extracts it. Calendar dates are then converted to order-preserving integers
% of the form $10000y+100m+d$. A smaller value is overdue and an equal value
% is due now. The two |etoolbox| Booleans are the only facts the card
% renderer consumes.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_load_reference_date:
  {
    \tl_set:Nx \l__kanban_reference_tl {\kanban@thereference}
    \tl_if_blank:VTF \l__kanban_reference_tl
      {
        \int_set:Nn \l__kanban_reference_year_int {\year}
        \int_set:Nn \l__kanban_reference_month_int {\month}
        \int_set:Nn \l__kanban_reference_day_int {\day}
      }
      {
        \exp_args:NnV \regex_extract_once:nnN
          {\A([0-9]{4})-([0-9]{2})-([0-9]{2})\Z}
          \l__kanban_reference_tl \l__kanban_deadline_match_seq
        \int_set:Nn \l__kanban_reference_year_int
          {\seq_item:Nn \l__kanban_deadline_match_seq {2}}
        \int_set:Nn \l__kanban_reference_month_int
          {\seq_item:Nn \l__kanban_deadline_match_seq {3}}
        \int_set:Nn \l__kanban_reference_day_int
          {\seq_item:Nn \l__kanban_deadline_match_seq {4}}
      }
  }
\cs_new_protected:Npn \__kanban_classify_calendar_date:
  {
    \__kanban_load_reference_date:
    \int_set:Nn \l__kanban_deadline_key_int
      {\l__kanban_deadline_year_int * 10000
        + \l__kanban_deadline_month_int * 100
        + \l__kanban_deadline_day_int}
    \int_set:Nn \l__kanban_reference_key_int
      {\l__kanban_reference_year_int * 10000
        + \l__kanban_reference_month_int * 100
        + \l__kanban_reference_day_int}
    \int_compare:nNnTF
      {\l__kanban_deadline_key_int} < {\l__kanban_reference_key_int}
      {\booltrue{kb@overdue}}
      {
        \int_compare:nNnT
          {\l__kanban_deadline_key_int} = {\l__kanban_reference_key_int}
          {\booltrue{kb@duenow}}
      }
  }
%    \end{macrocode}
%
% \paragraph{Project-month classification.}
% A project month begins on the configured start-day number, not necessarily
% on the first day of a calendar month. The classifier computes the nominal
% month offset from the project start, clamps the boundary day to the last
% valid day of a shorter reference month, and subtracts one when the
% reference date has not yet reached that boundary. The reference year and
% month are placed temporarily in the date component variables so that the
% shared maximum-day function can be reused.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_classify_project_month:
  {
    \__kanban_load_reference_date:
    \int_set:Nn \l__kanban_current_month_int
      {(\l__kanban_reference_year_int
          - \l__kanban_project_start_year_int) * 12
        + (\l__kanban_reference_month_int
          - \l__kanban_project_start_month_int) + 1}
    \int_set_eq:NN \l__kanban_deadline_year_int
      \l__kanban_reference_year_int
    \int_set_eq:NN \l__kanban_deadline_month_int
      \l__kanban_reference_month_int
    \__kanban_set_max_day:
    \int_set_eq:NN \l__kanban_project_boundary_day_int
      \l__kanban_project_start_day_int
    \int_compare:nNnT
      {\l__kanban_project_boundary_day_int}
      > {\l__kanban_deadline_max_day_int}
      {\int_set_eq:NN \l__kanban_project_boundary_day_int
        \l__kanban_deadline_max_day_int}
    \int_compare:nNnT
      {\l__kanban_reference_day_int}
      < {\l__kanban_project_boundary_day_int}
      {\int_decr:N \l__kanban_current_month_int}
    \int_compare:nNnTF
      {\l__kanban_current_month_int}
      > {\l__kanban_project_month_int}
      {\booltrue{kb@overdue}}
      {
        \int_compare:nNnT
          {\l__kanban_current_month_int}
          = {\l__kanban_project_month_int}
          {\booltrue{kb@duenow}}
      }
  }
%    \end{macrocode}
%
% \begin{macro}{\kb@evaloverdue}
% This internal entry point reduces every deadline value to two
% renderer-facing facts, overdue and due now. It resets both facts, tries the
% calendar grammar first, and otherwise distinguishes a project month from a
% neutral label. A project month is classified only when the board has a
% valid start-date snapshot. The function issues no diagnostics, because
% every invalid value has already been reported when its task was declared.
%    \begin{macrocode}
\cs_new_protected:Npn \kb@evaloverdue #1
  {
    \boolfalse{kb@overdue}
    \boolfalse{kb@duenow}
    \__kanban_parse_date:nn {#1} {\kanban@boarddateformat}
    \bool_if:NTF \l__kanban_valid_date_bool
      {\__kanban_classify_calendar_date:}
      {
        \bool_if:NF \l__kanban_date_syntax_bool
          {
            \__kanban_parse_project_month:n {#1}
            \bool_if:NT \l__kanban_valid_project_month_bool
              {
                \__kanban_parse_date:nn
                  {\kanban@thestart} {\kanban@boarddateformat}
                \bool_if:NT \l__kanban_valid_date_bool
                  {
                    \int_set_eq:NN \l__kanban_project_start_day_int
                      \l__kanban_deadline_day_int
                    \int_set_eq:NN \l__kanban_project_start_month_int
                      \l__kanban_deadline_month_int
                    \int_set_eq:NN \l__kanban_project_start_year_int
                      \l__kanban_deadline_year_int
                    \__kanban_classify_project_month:
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \paragraph{Deadline diagnostics at declaration time.}
% The collector calls this check for every accepted task that carries a
% deadline. A value with date syntax that fails validation, and a value with
% project-month syntax that lies outside the accepted range, each produce one
% named warning. Because the check runs while the task declaration is being
% read, the warning carries the line number of the task. Descriptive labels
% produce no diagnostic.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_deadline_diagnose:n #1
  {
    \__kanban_parse_date:nn {#1} {\kanban@boarddateformat}
    \bool_if:NF \l__kanban_valid_date_bool
      {
        \bool_if:NTF \l__kanban_date_syntax_bool
          {\msg_warning:nnn {kanban-board}{invalid-deadline}{#1}}
          {
            \__kanban_parse_project_month:n {#1}
            \bool_if:nT
              {
                \l__kanban_project_month_syntax_bool
                && ! \l__kanban_valid_project_month_bool
              }
              {\msg_warning:nnn {kanban-board}{invalid-project-month}{#1}}
          }
      }
  }
\cs_new_protected:Npn \kanban@diagnosedeadline #1
  {\tl_if_blank:nF {#1} {\__kanban_deadline_diagnose:n {#1}}}
%    \end{macrocode}
%
% \paragraph{Validated persistent and board-local date setters.}
% Start and reference dates enter the package through several configuration
% scopes, but every accepted non-empty value is stored canonically. The
% persistent setters update the defaults for future boards globally. The
% board setters update only the current environment. Empty values clear the
% corresponding setting. Invalid non-empty values keep the previously
% accepted value and issue the shared named warning. The expanded definitions
% store a self-contained canonical date rather than a reference to the
% mutable parsing integers.
%    \begin{macrocode}
\cs_new_protected:Npn \kanban@setstartdate #1
  {
    \tl_if_blank:nTF {#1}
      {\gdef\kanban@startdate{}}
      {
        \__kanban_parse_date:nn {#1} {\kanban@dateformat}
        \bool_if:NTF \l__kanban_valid_date_bool
          {\xdef\kanban@startdate{\__kanban_canonical_date:}}
          {\msg_warning:nnnn {kanban-board}{invalid-date-setting}
            {startdate}{#1}}
      }
  }
\cs_new_protected:Npn \kanban@setboardstartdate #1
  {
    \tl_if_blank:nTF {#1}
      {\def\kanban@thestart{}}
      {
        \__kanban_parse_date:nn {#1} {\kanban@boarddateformat}
        \bool_if:NTF \l__kanban_valid_date_bool
          {\edef\kanban@thestart{\__kanban_canonical_date:}}
          {\msg_warning:nnnn {kanban-board}{invalid-date-setting}
            {board~startdate}{#1}}
      }
  }
\cs_new_protected:Npn \kanban@setreferencedate #1
  {
    \tl_if_blank:nTF {#1}
      {\gdef\kanban@referencedate{}}
      {
        \__kanban_parse_date:nn {#1} {\kanban@dateformat}
        \bool_if:NTF \l__kanban_valid_date_bool
          {\xdef\kanban@referencedate{\__kanban_canonical_date:}}
          {\msg_warning:nnnn {kanban-board}{invalid-date-setting}
            {reference-date}{#1}}
      }
  }
\cs_new_protected:Npn \kanban@setboardreferencedate #1
  {
    \tl_if_blank:nTF {#1}
      {\def\kanban@thereference{}}
      {
        \__kanban_parse_date:nn {#1} {\kanban@boarddateformat}
        \bool_if:NTF \l__kanban_valid_date_bool
          {\edef\kanban@thereference{\__kanban_canonical_date:}}
          {\msg_warning:nnnn {kanban-board}{invalid-date-setting}
            {board~reference-date}{#1}}
      }
  }
%    \end{macrocode}
%
% \paragraph{Safe dimension recognition.}
% The column-gap setters must reject malformed input before TeX attempts a
% dimension assignment, because an unrestricted assignment can consume later
% document tokens while searching for a unit. The validator accepts three
% forms. The first is a complete explicit dimension in a standard TeX unit,
% with optional spaces before the unit and an optional |true| prefix on the
% physical units. The second is one existing dimension or skip register. The
% third is one token whose meaning is a dimension or glue parameter of TeX,
% such as |\parindent| or |\baselineskip|. Those parameters are primitives
% rather than registers, so the register tests do not recognize them. The
% constant lists the meaning of all 21 dimension parameters and all 15 glue
% parameters of TeX, and the membership test compares the meaning of the
% candidate token, which also accepts a |\let| copy of a parameter. The
% three math glue parameters are excluded because they cannot be assigned
% to a length. The validator does not evaluate arbitrary macros or
% dimension expressions at this boundary. The input is expanded through
% |\text_expand:n|, so a macro that expands to a dimension is accepted while
% a robust command is left intact and rejected. The two LaTeX2e-facing
% wrappers translate the conditional and the named message for the
% configuration layer.
%    \begin{macrocode}
\str_const:Nx \c__kanban_tex_lengths_str
  {
    | \token_to_meaning:N \parindent
    | \token_to_meaning:N \mathsurround
    | \token_to_meaning:N \lineskiplimit
    | \token_to_meaning:N \hsize
    | \token_to_meaning:N \vsize
    | \token_to_meaning:N \maxdepth
    | \token_to_meaning:N \splitmaxdepth
    | \token_to_meaning:N \boxmaxdepth
    | \token_to_meaning:N \hfuzz
    | \token_to_meaning:N \vfuzz
    | \token_to_meaning:N \delimitershortfall
    | \token_to_meaning:N \nulldelimiterspace
    | \token_to_meaning:N \scriptspace
    | \token_to_meaning:N \predisplaysize
    | \token_to_meaning:N \displaywidth
    | \token_to_meaning:N \displayindent
    | \token_to_meaning:N \overfullrule
    | \token_to_meaning:N \hangindent
    | \token_to_meaning:N \hoffset
    | \token_to_meaning:N \voffset
    | \token_to_meaning:N \emergencystretch
    | \token_to_meaning:N \lineskip
    | \token_to_meaning:N \baselineskip
    | \token_to_meaning:N \parskip
    | \token_to_meaning:N \abovedisplayskip
    | \token_to_meaning:N \abovedisplayshortskip
    | \token_to_meaning:N \belowdisplayskip
    | \token_to_meaning:N \belowdisplayshortskip
    | \token_to_meaning:N \leftskip
    | \token_to_meaning:N \rightskip
    | \token_to_meaning:N \topskip
    | \token_to_meaning:N \splittopskip
    | \token_to_meaning:N \tabskip
    | \token_to_meaning:N \spaceskip
    | \token_to_meaning:N \xspaceskip
    | \token_to_meaning:N \parfillskip |
  }
\prg_new_protected_conditional:Npnn
  \__kanban_dimension_token:N #1 {T,F,TF}
  {
    \token_if_dim_register:NTF #1
      {\prg_return_true:}
      {
        \token_if_skip_register:NTF #1
          {\prg_return_true:}
          {
            \exp_args:NNx \str_if_in:NnTF \c__kanban_tex_lengths_str
              { | \token_to_meaning:N #1 | }
              {\prg_return_true:}
              {\prg_return_false:}
          }
      }
  }
\prg_new_protected_conditional:Npnn
  \__kanban_dimension_valid:n #1 {T,F,TF}
  {
    \tl_set:Nx \l__kanban_dimension_tl {\text_expand:n {#1}}
    \tl_trim_spaces:N \l__kanban_dimension_tl
    \exp_args:NnV \regex_match:nnTF
      {\A[+\-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)\s*
        (?:(?:true\s*)?(?:pt|pc|in|bp|cm|mm|dd|cc|sp)|ex|em)\Z}
      \l__kanban_dimension_tl
      {\prg_return_true:}
      {
        \exp_args:NV \tl_if_single_token:nTF \l__kanban_dimension_tl
          {
            \exp_args:NV \__kanban_dimension_token:NTF
              \l__kanban_dimension_tl
              {\prg_return_true:}
              {\prg_return_false:}
          }
          {\prg_return_false:}
      }
  }
\cs_new_protected:Npn \kanban@validatedimensionTF #1#2#3
  {\__kanban_dimension_valid:nTF {#1}{#2}{#3}}
\cs_new_protected:Npn \kanban@dimensionwarning #1#2
  {\msg_warning:nnnn {kanban-board}{invalid-dimension}{#1}{#2}}
\ExplSyntaxOff
%    \end{macrocode}
%
%    \begin{macrocode}
%</package>
%    \end{macrocode}
