% \iffalse % Copyright (C) 2026 Jonathan LAFARGE % Development of this package was assisted by OpenAI ChatGPT; the model used % for the current development cycle was GPT-5.6 Sol (August 2026). % % 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 is available from % https://www.latex-project.org/lppl/. % % This work has the LPPL maintenance status `author-maintained'. % % This work consists of the files integramme.dtx and integramme.ins and the % derived file integramme.sty. % %<*driver> \documentclass{l3doc} \usepackage{integramme} \usepackage{booktabs} \usepackage{tabularx} \usepackage{enumitem} \OnlyDescription \begin{document} \DocInput{integramme.dtx} \end{document} % % \fi % % \GetFileInfo{integramme.sty} % % \title{The \textsf{integramme} package\\ % \large Typesetting logic-grid puzzles in \LaTeX} % \author{Jonathan LAFARGE} % \date{Version \fileversion\ -- \filedate} % % \maketitle % % \begin{abstract} % \textsf{integramme} typesets staircase-style logic-grid puzzles, including % Einstein-style puzzles. The package separates the logical data from the % drawing: authors declare categories, items, clues and, optionally, a solution; % the grid is generated automatically. Printed labels may contain ordinary % \LaTeX{} material while stable internal identifiers are used for references. % \end{abstract} % % \section{Status of version 0.9.0} % % Version 0.9.0 is the first release candidate. The public long-form API and % the ordinary rendering defaults are frozen for the planned 1.0 release except % for serious defects. Compatibility syntax from earlier prototypes is % retained but should not be used in new documents. % % Jonathan LAFARGE is the author, copyright holder and CTAN maintainer. The % work keeps LPPL status \texttt{author-maintained}. Public repository, bug % tracker and support URLs are optional CTAN communication metadata and are not % invented for this release candidate; they may be added later. % % \section{Installation} % % During development, place \texttt{integramme.sty} where \LaTeX{} can find it. % To generate the style file from the documented source, run: % % \begin{verbatim} % latex integramme.ins % \end{verbatim} % % For a local installation, move the resulting \texttt{integramme.sty} to an % appropriate directory in your local TEXMF tree and refresh the filename % database if your \TeX{} distribution requires it. % % The runtime dependency is only TikZ. The package requires a LaTeX2e format % dated 2022-06-01 or later. % % \section{Quick start} % % A minimal blank puzzle is declared by listing categories. Every category % currently has to contain the same number of items. % % \begin{verbatim} % \begin{integramme} % \IntegrammeCategory{person}{Person}{ % alice={Alice}, % bruno={Bruno}, % clara={Clara} % } % \IntegrammeCategory{drink}{Drink}{ % tea={Tea}, % coffee={Coffee}, % juice={Juice} % } % \IntegrammeCategory{pet}{Pet}{ % cat={Cat}, % dog={Dog}, % rabbit={Rabbit} % } % \end{integramme} % \end{verbatim} % % The result is generated from the data; no TikZ coordinates are needed. % % \begin{center} % \begin{integramme}[cell-size=5.5mm,side-label-min-width=18mm] % \IntegrammeCategory{person}{Person}{ % alice={Alice},bruno={Bruno},clara={Clara} % } % \IntegrammeCategory{drink}{Drink}{ % tea={Tea},coffee={Coffee},juice={Juice} % } % \IntegrammeCategory{pet}{Pet}{ % cat={Cat},dog={Dog},rabbit={Rabbit} % } % \end{integramme} % \end{center} % % \section{Einstein's riddle} % % The well-known ``Einstein riddle'', also called the five-houses puzzle, is a % natural stress test for a logic-grid package. Its attribution to Albert % Einstein is not established; the variant used here follows the data described % by the French Wikipedia article ``Énigme d'Einstein'', consulted on % 2026-08-23. The clue wording below is independently paraphrased rather than % copied from that page. % % \begin{quote} % \small % Reference: % \href{https://fr.wikipedia.org/wiki/\%C3\%89nigme_d\%27Einstein} % {French Wikipedia -- Énigme d'Einstein}. % \end{quote} % % Since version 0.8.1, the package includes this classic puzzle as a convenience % preset. The preset only declares ordinary categories, clues and solution % rows; it does not introduce a different drawing engine or solving algorithm. % % \begin{verbatim} % \begin{integramme}[ % preset=einstein, % preset-language=fr % ] % \end{integramme} % \end{verbatim} % % The same preset can be rendered in English with % \texttt{preset-language=en}. The default preset language is English. % Passing \texttt{mode=solution} prints the complete solved grid. % % The full puzzle contains six categories of five items, including house % position, so it is substantially larger than the quick-start example. The % following compact settings keep the demonstration inside the manual: % % \begin{center} % \begin{integramme}[ % preset=einstein, % preset-language=fr, % cell-size=3.3mm, % side-title-width=5.5mm, % side-label-min-width=18mm, % font=\scriptsize\sffamily, % title-font=\scriptsize\sffamily\bfseries, % indices-font=\footnotesize, % overflow-warning=false % ] % \end{integramme} % \end{center} % % In this variant the German owns the fish. A standalone, more generously % spaced version is shipped as \texttt{example-einstein.tex}. % % \section{Categories and items} % % \begin{function}{\IntegrammeCategory} % \begin{syntax} % \cs{IntegrammeCategory}\marg{category id}\marg{printed title}\marg{items} % \end{syntax} % \end{function} % % New documents should give every item an internal identifier and a printed % label: % % \begin{verbatim} % \IntegrammeCategory{city}{City}{ % st_etienne={Saint-Étienne}, % calais={Calais}, % frejus={Fréjus} % } % \end{verbatim} % % Internal identifiers are used by manual marks and solutions. Printed labels % are independent and may contain commas, slashes, mathematics and ordinary % formatting commands. For maximum robustness, keep identifiers short and % ASCII-like and avoid commas and equals signs in identifiers. % % The older shorthand % % \begin{verbatim} % \IntegrammeCategory{city}{City}{Calais,Fréjus,Annecy} % \end{verbatim} % % remains accepted for compatibility; in that form the identifier and printed % label are the same token list. % % \section{Clues} % % \begin{function}{\IntegrammeIndices} % \begin{syntax} % \cs{IntegrammeIndices}\marg{item list} % \end{syntax} % \end{function} % % Clues are written as ordinary \cs{item} entries. They are printed as a % numbered list below the grid. % % \begin{verbatim} % \IntegrammeIndices{% % \item Alice does not drink tea. % \item The owner of the rabbit drinks coffee. % \item Clara is not the owner of the dog. % } % \end{verbatim} % % Because the argument contains normal list items, clue text may contain commas, % semicolons and mathematics without introducing a package-level separator. % % \section{Modes} % % The key \texttt{mode} selects what is printed in the cells. % % \begin{description}[style=nextline] % \item[\texttt{mode=blank}] Prints an empty pupil grid. This is the default. % \item[\texttt{mode=manual}] Prints only marks explicitly declared with % \cs{IntegrammeYes} and \cs{IntegrammeNo}. % \item[\texttt{mode=solution}] Prints a complete positive/negative grid derived % from declared solution rows. % \end{description} % % \subsection{Manual marks} % % \begin{function}{\IntegrammeYes,\IntegrammeNo} % \begin{syntax} % \cs{IntegrammeYes}\marg{category=item}\marg{category=item} % \cs{IntegrammeNo}\marg{category=item}\marg{category=item} % \end{syntax} % \end{function} % % Example: % % \begin{verbatim} % \begin{integramme}[mode=manual] % ... categories ... % \IntegrammeNo {person=alice}{drink=tea} % \IntegrammeYes{person=bruno}{pet=dog} % \end{integramme} % \end{verbatim} % % The two references may be supplied in either order. The package reports an % error if a reference is unknown, compares two items from the same category, or % declares the same cell both positive and negative. % % \subsection{Complete solutions} % % \begin{function}{\IntegrammeSolutionRow} % \begin{syntax} % \cs{IntegrammeSolutionRow}\marg{category=item,...} % \end{syntax} % \end{function} % % The recommended solution syntax is keyed and order-independent: % % \begin{verbatim} % \begin{integramme}[mode=solution] % ... categories ... % \IntegrammeSolutionRow{ % person=alice, drink=tea, pet=cat % } % \IntegrammeSolutionRow{ % pet=dog, person=bruno, drink=coffee % } % \IntegrammeSolutionRow{ % drink=juice, pet=rabbit, person=clara % } % \end{integramme} % \end{verbatim} % % Every solution row must assign exactly one item from every category. A % complete solution must contain exactly as many rows as there are items in each % category, and every item must occur exactly once per category. % % \section{Configuration} % % Package settings can be set globally with \cs{integrammeset} and overridden % locally in the optional argument of the environment. % % \begin{verbatim} % \integrammeset{ % cell-size=6mm, % column-labels=auto, % top-angle=45 % } % % \begin{integramme}[cell-size=5mm] % ... % \end{integramme} % \end{verbatim} % % \subsection{Geometry and rules} % % \begin{tabularx}{\linewidth}{@{}>{\ttfamily}l >{\ttfamily}l X@{}} % \toprule % Key & \normalfont Default & Meaning \\ % \midrule % cell-size & 6mm & \normalfont Width and height of one logical cell. \\ % rule-width & 0.3pt & \normalfont Width of thin internal grid lines. \\ % block-rule-width & 1pt & \normalfont Width of category/block borders. \\ % side-title-width & 7mm & \normalfont Width reserved for vertical category titles. \\ % side-label-width & 0pt & \normalfont Fixed row-label width; \texttt{0pt} requests automatic measurement. \\ % side-label-min-width & 26mm & \normalfont Minimum automatic row-label width. \\ % side-padding & 1.5mm & \normalfont Horizontal padding inside the row-label area. \\ % top-padding & 1mm & \normalfont Padding used in the top-label area. \\ % top-label-min-height & 7mm & \normalfont Minimum height of the top item-label area. \\ % title-height & 7mm & \normalfont Height of the top category-title row. \\ % indices-sep & 3mm & \normalfont Space between the grid and the clue list. \\ % guide-stem-height & 0pt & \normalfont Vertical stem before a slanted guide; \texttt{0pt} means automatic. \\ % \bottomrule % \end{tabularx} % % \subsection{Header behaviour} % % \begin{tabularx}{\linewidth}{@{}>{\ttfamily}l >{\ttfamily}l X@{}} % \toprule % Key & \normalfont Default & Meaning \\ % \midrule % column-labels & auto & \normalfont \texttt{auto}, \texttt{horizontal} or \texttt{slanted}. In \texttt{auto}, all top labels become slanted if at least one does not fit horizontally. \\ % top-angle & 45 & \normalfont Angle, in degrees, used for slanted top labels. \\ % guides & true & \normalfont Shows or hides the top label guides. \\ % guide-length & 1.15 & \normalfont Slanted guide length, measured in multiples of \texttt{cell-size}. \\ % guide-opacity & 0.45 & \normalfont Opacity of the top guides. \\ % \bottomrule % \end{tabularx} % % \subsection{Typography, colours and marks} % % \begin{tabularx}{\linewidth}{@{}>{\ttfamily}l >{\ttfamily}l X@{}} % \toprule % Key & \normalfont Default & Meaning \\ % \midrule % font & \cs{sffamily} & \normalfont Font commands used for ordinary item labels. \\ % title-font & \cs{sffamily}\cs{bfseries} & \normalfont Font commands used for category titles. \\ % indices-font & \cs{normalfont} & \normalfont Font commands used for the clue list. \\ % mark-font & \cs{sffamily} & \normalfont Font commands used for positive/negative marks. \\ % yes-mark & \cs{bullet} & \normalfont Material printed for a positive cell (math-safe by default). \\ % no-mark & \cs{times} & \normalfont Material printed for a negative cell (math-safe by default). \\ % grid-color & black & \normalfont TikZ colour used for the grid. \\ % text-color & black & \normalfont TikZ colour used for labels and marks. \\ % \bottomrule % \end{tabularx} % % \subsection{Behaviour} % % \begin{tabularx}{\linewidth}{@{}>{\ttfamily}l >{\ttfamily}l X@{}} % \toprule % Key & \normalfont Default & Meaning \\ % \midrule % mode & blank & \normalfont \texttt{blank}, \texttt{manual} or \texttt{solution}; selects cell content. \\ % preset & none & \normalfont \texttt{none} or \texttt{einstein}; preloads a complete built-in puzzle data set. \\ % preset-language & en & \normalfont \texttt{en} or \texttt{fr}; language used by built-in preset labels and clues. \\ % overflow-warning & true & \normalfont Warns when the complete puzzle is wider than the current \cs{linewidth}. \\ % \bottomrule % \end{tabularx} % % The package never scales a puzzle silently. If the width warning fires, % authors remain in control of cell size, label width and surrounding layout. % % \section{Compatibility syntax} % % The following forms are still accepted but are not recommended for new % documents: % % \begin{verbatim} % \IntegrammeCategory{person}{Person}{Alice,Bruno,Clara} % \IntegrammeNo{person/Alice}{pet/Cat} % \IntegrammeSolutionRow{Alice,Tea,Cat} % \IntegrammeSolution{Alice/Tea/Cat;Bruno/Coffee/Dog;Clara/Juice/Rabbit} % \end{verbatim} % % Short aliases beginning with \cs{Int} also remain available for compatibility % and convenience, but the long-form commands are the candidate stable API. % % \section{Diagnostics and current limitations} % % Version 0.7.0 and later validate the data before drawing. Among other checks, it reports % duplicate category or item identifiers, unequal category sizes, malformed or % unknown references, conflicting manual marks, incomplete solution rows and % duplicate use of solution items. Category identifiers are trimmed before % storage, an empty category identifier is rejected explicitly, and a % non-positive \texttt{cell-size} is rejected before the layout code can divide % by it. % % Current limitations are: % \begin{itemize} % \item all categories must have the same number of items; % \item only the staircase layout is implemented; % \item long slanted top labels are not automatically constrained to their own % category block; % \item identifiers should remain simple and should avoid commas and equals % signs. % \end{itemize} % % \section{Regression testing} % % Version 0.9.0 runs the release-candidate regression suite on pdfTeX, LuaTeX and XeTeX. The suite % checks the candidate public surface and defaults in addition to canonical and % compatibility syntax, global/local option scoping, rich labels, representative % puzzle sizes, warnings and every package-defined error path. Diagnostic tests % redirect package errors to warnings so that complete transcripts can be % compared by \texttt{l3build} without stopping the test run. % % \section{Building the package} % % The source tree now follows the conventional DocStrip/\texttt{l3build} % pattern. Useful commands are: % % \begin{verbatim} % latex integramme.ins # generate integramme.sty % l3build unpack # generate installable files in the build tree % l3build doc # build integramme.pdf % l3build check # run regression tests % l3build ctan # build a CTAN-style archive % \end{verbatim} % % The release archive produced by \texttt{l3build ctan} is intended as the % basis for a future CTAN submission. Version 0.9.0 also ships a maintainer-side % static audit in the development tree; it checks version consistency, the % documented key/command surface and archive contents. Maintainer metadata is % finalized; optional repository and support URLs are omitted rather than invented. % % \section{Release metadata} % % CTAN requires a public maintainer name for a submission. Jonathan LAFARGE % is the public author, copyright holder and CTAN maintainer. This work keeps % LPPL status \texttt{author-}\allowbreak\texttt{maintained}, which reserves % maintenance to the copyright holder. Repository, bug-tracker and support % URLs are optional CTAN communication metadata and are intentionally omitted % rather than invented. No end-user command depends on these fields. % % \section{Authorship and AI-assisted development} % % The author and copyright holder of \textsf{integramme} is Jonathan LAFARGE. % OpenAI's ChatGPT was used as an interactive development assistant for code % drafting, refactoring, test generation and the drafting/revision of % documentation. The model used for the current development cycle was % \textbf{GPT-5.6 Sol} (August 2026). Design decisions, review, testing and % release responsibility remain with the human author. % % ChatGPT is therefore acknowledged as a development tool, not listed as an % author, copyright holder or CTAN maintainer. % % \section{License} % % This work may be distributed and/or modified under the conditions of the % LaTeX Project Public License, version 1.3c or later. The latest version of % this license is available from \texttt{https://www.latex-project.org/lppl/}. % The work has LPPL maintenance status \texttt{author-maintained}. % % \StopEventually{} % % \section{Implementation} % % The implementation remains based on LaTeX3 data structures and TikZ. Version % 0.9.0 is the release-candidate freeze: the ordinary rendering algorithm and % public long-form API are unchanged from the audited 0.8 series. % % \begin{macrocode} %<*package> \NeedsTeXFormat{LaTeX2e}[2022-06-01] \ProvidesExplPackage {integramme} {2026-08-23} {0.9.0} {Typeset logic-grid puzzles (integrammes)} \RequirePackage{tikz} \ExplSyntaxOn \cs_generate_variant:Nn \prop_item:Nn { Ne } % -------------------------------------------------------------------------- % Data % -------------------------------------------------------------------------- \seq_new:N \l__integramme_categories_seq \prop_new:N \l__integramme_titles_prop \prop_new:N \l__integramme_item_count_prop \prop_new:N \l__integramme_item_id_prop \prop_new:N \l__integramme_item_label_prop \prop_new:N \l__integramme_item_index_prop \seq_new:N \l__integramme_solution_seq % normalized rows, category order \seq_new:N \l__integramme_solution_input_seq % raw rows as entered by the user \seq_new:N \l__integramme_manual_yes_seq \seq_new:N \l__integramme_manual_no_seq \prop_new:N \l__integramme_manual_marks_prop \prop_new:N \l__integramme_solution_row_prop \tl_new:N \l__integramme_indices_tl \tl_new:N \l__integramme_current_category_tl \int_new:N \l__integramme_category_count_int \int_new:N \l__integramme_order_int \int_new:N \l__integramme_scan_int \int_new:N \l__integramme_current_item_int \int_new:N \l__integramme_draw_row_int \int_new:N \l__integramme_refa_cat_int \int_new:N \l__integramme_refa_item_int \int_new:N \l__integramme_refb_cat_int \int_new:N \l__integramme_refb_item_int \bool_new:N \l__integramme_valid_bool % -------------------------------------------------------------------------- % Configuration % -------------------------------------------------------------------------- \dim_new:N \l__integramme_cell_dim \dim_new:N \l__integramme_rule_dim \dim_new:N \l__integramme_block_rule_dim \dim_new:N \l__integramme_side_title_dim \dim_new:N \l__integramme_side_label_dim \dim_new:N \l__integramme_side_label_min_dim \dim_new:N \l__integramme_side_padding_dim \dim_new:N \l__integramme_top_padding_dim \dim_new:N \l__integramme_top_label_min_dim \dim_new:N \l__integramme_top_label_dim \dim_new:N \l__integramme_title_height_dim \dim_new:N \l__integramme_indices_sep_dim \dim_new:N \l__integramme_guide_stem_dim \dim_new:N \l__integramme_guide_stem_effective_dim \dim_new:N \l__integramme_total_width_dim \tl_new:N \l__integramme_font_tl \tl_new:N \l__integramme_title_font_tl \tl_new:N \l__integramme_mode_tl \tl_new:N \l__integramme_yes_tl \tl_new:N \l__integramme_no_tl \tl_new:N \l__integramme_grid_color_tl \tl_new:N \l__integramme_text_color_tl \tl_new:N \l__integramme_column_labels_tl \tl_new:N \l__integramme_indices_font_tl \tl_new:N \l__integramme_mark_font_tl \tl_new:N \l__integramme_preset_tl \tl_new:N \l__integramme_preset_language_tl \fp_new:N \l__integramme_top_angle_fp \fp_new:N \l__integramme_guide_length_fp \fp_new:N \l__integramme_guide_opacity_fp \bool_new:N \l__integramme_top_slanted_bool \bool_new:N \l__integramme_guides_bool \bool_new:N \l__integramme_overflow_warning_bool \keys_define:nn { integramme } { cell-size .dim_set:N = \l__integramme_cell_dim, cell-size .initial:n = 6mm, rule-width .dim_set:N = \l__integramme_rule_dim, rule-width .initial:n = 0.3pt, block-rule-width .dim_set:N = \l__integramme_block_rule_dim, block-rule-width .initial:n = 1pt, side-title-width .dim_set:N = \l__integramme_side_title_dim, side-title-width .initial:n = 7mm, side-label-width .dim_set:N = \l__integramme_side_label_dim, side-label-width .initial:n = 0pt, side-label-min-width .dim_set:N = \l__integramme_side_label_min_dim, side-label-min-width .initial:n = 26mm, side-padding .dim_set:N = \l__integramme_side_padding_dim, side-padding .initial:n = 1.5mm, top-padding .dim_set:N = \l__integramme_top_padding_dim, top-padding .initial:n = 1mm, top-label-min-height .dim_set:N = \l__integramme_top_label_min_dim, top-label-min-height .initial:n = 7mm, title-height .dim_set:N = \l__integramme_title_height_dim, title-height .initial:n = 7mm, indices-sep .dim_set:N = \l__integramme_indices_sep_dim, indices-sep .initial:n = 3mm, top-angle .fp_set:N = \l__integramme_top_angle_fp, top-angle .initial:n = 45, guide-length .fp_set:N = \l__integramme_guide_length_fp, guide-length .initial:n = 1.15, % Height of the short vertical part before a slanted separator turns. % 0pt means automatic: approximately one capital-letter height. guide-stem-height .dim_set:N = \l__integramme_guide_stem_dim, guide-stem-height .initial:n = 0pt, guide-opacity .fp_set:N = \l__integramme_guide_opacity_fp, guide-opacity .initial:n = 0.45, guides .bool_set:N = \l__integramme_guides_bool, guides .initial:n = true, overflow-warning .bool_set:N = \l__integramme_overflow_warning_bool, overflow-warning .initial:n = true, font .tl_set:N = \l__integramme_font_tl, font .initial:n = \sffamily, title-font .tl_set:N = \l__integramme_title_font_tl, title-font .initial:n = \sffamily\bfseries, indices-font .tl_set:N = \l__integramme_indices_font_tl, indices-font .initial:n = \normalfont, mark-font .tl_set:N = \l__integramme_mark_font_tl, mark-font .initial:n = \sffamily, grid-color .tl_set:N = \l__integramme_grid_color_tl, grid-color .initial:n = black, text-color .tl_set:N = \l__integramme_text_color_tl, text-color .initial:n = black, yes-mark .tl_set:N = \l__integramme_yes_tl, yes-mark .initial:n = \ensuremath{\bullet}, no-mark .tl_set:N = \l__integramme_no_tl, no-mark .initial:n = \ensuremath{\times}, column-labels .choice:, column-labels / auto .code:n = { \tl_set:Nn \l__integramme_column_labels_tl { auto } }, column-labels / horizontal .code:n = { \tl_set:Nn \l__integramme_column_labels_tl { horizontal } }, column-labels / slanted .code:n = { \tl_set:Nn \l__integramme_column_labels_tl { slanted } }, column-labels .initial:n = auto, preset .choice:, preset / none .code:n = { \tl_set:Nn \l__integramme_preset_tl { none } }, preset / einstein .code:n = { \tl_set:Nn \l__integramme_preset_tl { einstein } }, preset .initial:n = none, preset-language .choice:, preset-language / en .code:n = { \tl_set:Nn \l__integramme_preset_language_tl { en } }, preset-language / fr .code:n = { \tl_set:Nn \l__integramme_preset_language_tl { fr } }, preset-language .initial:n = en, mode .choice:, mode / blank .code:n = { \tl_set:Nn \l__integramme_mode_tl { blank } }, mode / manual .code:n = { \tl_set:Nn \l__integramme_mode_tl { manual } }, mode / solution .code:n = { \tl_set:Nn \l__integramme_mode_tl { solution } }, mode .initial:n = blank, } \NewDocumentCommand \integrammeset { m } { \keys_set:nn { integramme } {#1} } % -------------------------------------------------------------------------- % Messages % -------------------------------------------------------------------------- \msg_new:nnn { integramme } { duplicate-category } { The~category~identifier~'#1'~is~already~used. } \msg_new:nnn { integramme } { empty-category-id } { A~category~must~have~a~non-empty~identifier. } \msg_new:nnn { integramme } { duplicate-item } { The~item~identifier~'#1'~is~already~used~in~category~'#2'. } \msg_new:nnn { integramme } { empty-item-id } { Category~'#1'~contains~an~item~with~an~empty~identifier. } \msg_new:nnn { integramme } { empty-category } { Category~'#1'~must~contain~at~least~one~item. } \msg_new:nnn { integramme } { too-few-categories } { An~integramme~needs~at~least~two~categories. } \msg_new:nnn { integramme } { unequal-sizes } { All~categories~must~have~the~same~number~of~items.~Category~'#1'~does~not. } \msg_new:nnn { integramme } { missing-solution } { mode=solution~was~requested,~but~no~solution~was~declared. } \msg_new:nnn { integramme } { bad-solution-size } { The~solution~must~contain~exactly~#1~rows. } \msg_new:nnn { integramme } { bad-solution-row } { Each~positional~solution~row~must~contain~exactly~#1~entries. } \msg_new:nnn { integramme } { bad-keyed-solution-entry } { Solution~row~entry~'#1'~must~have~the~form~category=item. } \msg_new:nnn { integramme } { duplicate-solution-category } { Category~'#1'~is~assigned~more~than~once~in~the~same~solution~row. } \msg_new:nnn { integramme } { missing-solution-category } { Solution~row~does~not~assign~an~item~to~category~'#1'. } \msg_new:nnn { integramme } { duplicate-solution-item } { Solution~item~'#2'~occurs~more~than~once~in~category~'#1'. } \msg_new:nnn { integramme } { grid-too-wide } { The~integramme~is~#1~wide,~but~the~current~line~width~is~#2. } \msg_new:nnn { integramme } { nonpositive-cell-size } { The~key~'cell-size'~must~be~greater~than~0pt. } \msg_new:nnn { integramme } { bad-reference } { The~reference~'#1'~must~have~the~form~category=item~(or~legacy~category/item). } \msg_new:nnn { integramme } { unknown-category } { The~category~identifier~'#1'~does~not~exist. } \msg_new:nnn { integramme } { unknown-item } { The~item~'#2'~does~not~exist~in~category~'#1'. } \msg_new:nnn { integramme } { same-category-mark } { A~mark~cannot~compare~two~items~from~the~same~category~('#1'). } \msg_new:nnn { integramme } { conflicting-mark } { The~cell~'#1'~has~been~declared~both~positive~and~negative. } \msg_new:nnn { integramme } { manual-marks-ignored } { Manual~marks~were~declared,~but~mode=#1.~They~will~not~be~printed. } % -------------------------------------------------------------------------- % User data commands % -------------------------------------------------------------------------- % Since 0.4, item identifiers are separated from printed labels. % The third argument of \IntegrammeCategory is parsed as a key-value list: % Alice, Irene -> legacy shorthand (id = label) % alice={Alice}, irene={Irène} -> robust form (recommended) % Braces protect commas, slashes, mathematics and arbitrary LaTeX in labels. \cs_new:Npn \__integramme_item_key:nn #1#2 { #1 || #2 } \cs_new:Npn \__integramme_index_key:nn #1#2 { #1 / \int_eval:n {#2} } \cs_new_protected:Npn \__integramme_store_item:nn #1#2 { \tl_set:Nn \l_tmpa_tl {#1} \tl_trim_spaces:N \l_tmpa_tl \tl_set:Nn \l_tmpb_tl {#2} \tl_trim_spaces:N \l_tmpb_tl \tl_if_empty:NTF \l_tmpa_tl { \msg_error:nnx { integramme } { empty-item-id } { \tl_use:N \l__integramme_current_category_tl } \bool_set_false:N \l__integramme_valid_bool } { \tl_set:Nx \l_tmpc_tl { \__integramme_item_key:nn { \tl_use:N \l__integramme_current_category_tl } { \tl_use:N \l_tmpa_tl } } \prop_if_in:NVTF \l__integramme_item_index_prop \l_tmpc_tl { \msg_error:nnxx { integramme } { duplicate-item } { \tl_use:N \l_tmpa_tl } { \tl_use:N \l__integramme_current_category_tl } \bool_set_false:N \l__integramme_valid_bool } { \int_incr:N \l__integramme_current_item_int \prop_put:NVx \l__integramme_item_index_prop \l_tmpc_tl { \int_use:N \l__integramme_current_item_int } \tl_set:Nx \l_tmpc_tl { \__integramme_index_key:nn { \tl_use:N \l__integramme_current_category_tl } { \int_use:N \l__integramme_current_item_int } } \prop_put:NVV \l__integramme_item_id_prop \l_tmpc_tl \l_tmpa_tl \prop_put:NVV \l__integramme_item_label_prop \l_tmpc_tl \l_tmpb_tl } } } \cs_new_protected:Npn \__integramme_item_bare:n #1 { \__integramme_store_item:nn {#1}{#1} } \cs_new_protected:Npn \__integramme_item_keyval:nn #1#2 { \__integramme_store_item:nn {#1}{#2} } \cs_new_protected:Npn \__integramme_add_category:nnn #1#2#3 { % Category identifiers are structural data, so insignificant surrounding % spaces are normalized before the identifier is stored or compared. \tl_set:Nn \l_tmpa_tl {#1} \tl_trim_spaces:N \l_tmpa_tl \tl_if_empty:NTF \l_tmpa_tl { \msg_error:nn { integramme } { empty-category-id } \bool_set_false:N \l__integramme_valid_bool } { \seq_if_in:NVTF \l__integramme_categories_seq \l_tmpa_tl { \msg_error:nnx { integramme } { duplicate-category } { \tl_use:N \l_tmpa_tl } \bool_set_false:N \l__integramme_valid_bool } { \seq_put_right:NV \l__integramme_categories_seq \l_tmpa_tl \prop_put:NVn \l__integramme_titles_prop \l_tmpa_tl {#2} \tl_set:Nx \l__integramme_current_category_tl { \tl_use:N \l_tmpa_tl } \int_zero:N \l__integramme_current_item_int % The kernel key-value parser reports a generic error for an empty % left-hand key before our callbacks run. Detect that case first so % users receive an integramme-specific diagnostic instead. \regex_match:nnTF { (?: \A | , ) \s* = } {#3} { \msg_error:nnx { integramme } { empty-item-id } { \tl_use:N \l__integramme_current_category_tl } \bool_set_false:N \l__integramme_valid_bool } { \keyval_parse:NNn \__integramme_item_bare:n \__integramme_item_keyval:nn {#3} } \prop_put:NVx \l__integramme_item_count_prop \l__integramme_current_category_tl { \int_use:N \l__integramme_current_item_int } } } } \NewDocumentCommand \IntegrammeCategory { m m m } { \__integramme_add_category:nnn {#1}{#2}{#3} } \cs_new_eq:NN \IntCategory \IntegrammeCategory % Version 0.5 stabilizes the public data API around key-value input. % % Recommended solution syntax (order-independent): % \IntegrammeSolutionRow{person=alice,city=calais,age=a26} % % For backward compatibility, positional rows such as % \IntegrammeSolutionRow{alice,calais,a26} % and the legacy compact \IntegrammeSolution command are still accepted. % Raw rows are normalized only when the environment closes, so declarations % remain order-flexible. \NewDocumentCommand \IntegrammeSolution { m } { \seq_clear:N \l__integramme_solution_input_seq \seq_set_split:Nnn \l_tmpa_seq { ; } {#1} \seq_map_inline:Nn \l_tmpa_seq { \seq_set_split:Nnn \l_tmpb_seq {/} {##1} \tl_set:Nx \l_tmpa_tl { \seq_use:Nn \l_tmpb_seq {,} } \seq_put_right:NV \l__integramme_solution_input_seq \l_tmpa_tl } } \cs_new_eq:NN \IntSolution \IntegrammeSolution \NewDocumentCommand \IntegrammeSolutionRow { m } { \seq_put_right:Nn \l__integramme_solution_input_seq {#1} } \cs_new_eq:NN \IntSolutionRow \IntegrammeSolutionRow % The argument contains ordinary \item commands. Using LaTeX list syntax here % avoids reserving punctuation characters inside clues. \NewDocumentCommand \IntegrammeIndices { +m } { \tl_set:Nn \l__integramme_indices_tl {#1} } \cs_new_eq:NN \IntIndices \IntegrammeIndices % Recommended 0.5 manual-mark syntax keeps two explicit references: % \IntegrammeYes{person=alice}{age=a26} % \IntegrammeNo {person=alice}{city=annecy} % % The 0.3/0.4 category/item references remain accepted for compatibility. % Marks are resolved only when the environment closes, keeping input order % flexible. \NewDocumentCommand \IntegrammeYes { m m } { \seq_put_right:Nn \l__integramme_manual_yes_seq { {#1}{#2} } } \cs_new_eq:NN \IntYes \IntegrammeYes \NewDocumentCommand \IntegrammeNo { m m } { \seq_put_right:Nn \l__integramme_manual_no_seq { {#1}{#2} } } \cs_new_eq:NN \IntNo \IntegrammeNo % -------------------------------------------------------------------------- % Built-in presets % -------------------------------------------------------------------------- % Presets are convenience data sets. They deliberately use only the public % declaration commands, so preset puzzles exercise the same validation and % drawing paths as user-authored puzzles. \cs_new_protected:Npn \__integramme_preset_einstein_solution: { \IntegrammeSolutionRow { nationality=norwegian,house=h1,color=yellow,drink=water,cigar=dunhill,pet=cats } \IntegrammeSolutionRow { nationality=dane,house=h2,color=blue,drink=tea,cigar=blend,pet=horse } \IntegrammeSolutionRow { nationality=briton,house=h3,color=red,drink=milk,cigar=pallmall,pet=birds } \IntegrammeSolutionRow { nationality=german,house=h4,color=green,drink=coffee,cigar=prince,pet=fish } \IntegrammeSolutionRow { nationality=swede,house=h5,color=white,drink=beer,cigar=bluemaster,pet=dogs } } \cs_new_protected:Npn \__integramme_preset_einstein_en: { \IntegrammeCategory { nationality } { Nationality } { briton={Briton},swede={Swede},dane={Dane},norwegian={Norwegian},german={German} } \IntegrammeCategory { house } { House } { h1={House~1},h2={House~2},h3={House~3},h4={House~4},h5={House~5} } \IntegrammeCategory { color } { Color } { red={Red},green={Green},white={White},yellow={Yellow},blue={Blue} } \IntegrammeCategory { drink } { Drink } { tea={Tea},coffee={Coffee},milk={Milk},beer={Beer},water={Water} } \IntegrammeCategory { cigar } { Cigar } { pallmall={Pall~Mall},dunhill={Dunhill},blend={Blend},bluemaster={Bluemaster},prince={Prince} } \IntegrammeCategory { pet } { Pet } { dogs={Dogs},birds={Birds},cats={Cats},horse={Horse},fish={Fish} } \IntegrammeIndices { \item The~Briton~lives~in~the~red~house. \item The~Swede~owns~dogs. \item The~Dane~drinks~tea. \item The~green~house~is~immediately~to~the~left~of~the~white~house. \item Coffee~is~drunk~in~the~green~house. \item The~Pall~Mall~smoker~keeps~birds. \item Dunhill~is~smoked~in~the~yellow~house. \item Milk~is~drunk~in~the~middle~house. \item The~Norwegian~lives~in~the~first~house~from~the~left. \item The~Blend~smoker~lives~next~to~the~cat~owner. \item The~horse~owner~lives~next~to~the~Dunhill~smoker. \item The~Bluemaster~smoker~drinks~beer. \item The~German~smokes~Prince. \item The~Norwegian~lives~next~to~the~blue~house. \item The~Blend~smoker~has~a~neighbour~who~drinks~water. \item[\textbf{Question:}] Who~owns~the~fish? } \__integramme_preset_einstein_solution: } \cs_new_protected:Npn \__integramme_preset_einstein_fr: { \IntegrammeCategory { nationality } { Nationalité } { briton={Britannique},swede={Suédois},dane={Danois},norwegian={Norvégien},german={Allemand} } \IntegrammeCategory { house } { Maison } { h1={Maison~1},h2={Maison~2},h3={Maison~3},h4={Maison~4},h5={Maison~5} } \IntegrammeCategory { color } { Couleur } { red={Rouge},green={Verte},white={Blanche},yellow={Jaune},blue={Bleue} } \IntegrammeCategory { drink } { Boisson } { tea={Thé},coffee={Café},milk={Lait},beer={Bière},water={Eau} } \IntegrammeCategory { cigar } { Cigare } { pallmall={Pall~Mall},dunhill={Dunhill},blend={Blend},bluemaster={Bluemaster},prince={Prince} } \IntegrammeCategory { pet } { Animal } { dogs={Chiens},birds={Oiseaux},cats={Chats},horse={Cheval},fish={Poisson rouge} } \IntegrammeIndices { \item La~maison~rouge~est~celle~du~Britannique. \item Le~Suédois~possède~des~chiens. \item Le~Danois~boit~du~thé. \item La~maison~verte~se~trouve~immédiatement~à~gauche~de~la~maison~blanche. \item Dans~la~maison~verte,~on~boit~du~café. \item La~personne~qui~fume~des~Pall~Mall~possède~des~oiseaux. \item Les~Dunhill~sont~fumées~dans~la~maison~jaune. \item Du~lait~est~bu~dans~la~troisième~maison. \item Le~Norvégien~occupe~la~première~maison,~à~l'extrémité~gauche. \item Le~fumeur~de~Blend~habite~à~côté~de~la~personne~qui~possède~des~chats. \item La~personne~qui~possède~un~cheval~vit~à~côté~du~fumeur~de~Dunhill. \item Le~fumeur~de~Bluemaster~boit~de~la~bière. \item L'Allemand~fume~des~Prince. \item La~maison~du~Norvégien~est~voisine~de~la~maison~bleue. \item Le~fumeur~de~Blend~a~pour~voisin~quelqu'un~qui~boit~de~l'eau. \item[\textbf{Question~:}] Qui~possède~le~poisson~rouge~? } \__integramme_preset_einstein_solution: } \cs_new_protected:Npn \__integramme_apply_preset: { \tl_if_eq:NnT \l__integramme_preset_tl { einstein } { \tl_if_eq:NnTF \l__integramme_preset_language_tl { fr } { \__integramme_preset_einstein_fr: } { \__integramme_preset_einstein_en: } } } % -------------------------------------------------------------------------- % Manual-mark references % -------------------------------------------------------------------------- \cs_new_protected:Npn \__integramme_resolve_ids:nnNN #1#2#3#4 { \int_zero:N #3 \int_zero:N #4 \tl_set:Nn \l_tmpa_tl {#1} \tl_set:Nn \l_tmpb_tl {#2} \tl_trim_spaces:N \l_tmpa_tl \tl_trim_spaces:N \l_tmpb_tl \int_zero:N \l__integramme_scan_int \seq_map_inline:Nn \l__integramme_categories_seq { \int_incr:N \l__integramme_scan_int \tl_set:Nn \l_tmpc_tl {##1} \tl_if_eq:NNT \l_tmpc_tl \l_tmpa_tl { \int_set_eq:NN #3 \l__integramme_scan_int \seq_map_break: } } \int_compare:nNnTF {#3} = {0} { \msg_error:nnx { integramme } { unknown-category } { \tl_use:N \l_tmpa_tl } \bool_set_false:N \l__integramme_valid_bool } { \tl_set:Nx \l_tmpc_tl { \__integramme_item_key:nn { \tl_use:N \l_tmpa_tl } { \tl_use:N \l_tmpb_tl } } \prop_get:NVNTF \l__integramme_item_index_prop \l_tmpc_tl \l_tmpd_tl { \int_set:Nn #4 { \l_tmpd_tl } } { \msg_error:nnxx { integramme } { unknown-item } { \tl_use:N \l_tmpa_tl } { \tl_use:N \l_tmpb_tl } \bool_set_false:N \l__integramme_valid_bool } } } \cs_new_protected:Npn \__integramme_resolve_ref:nNN #1#2#3 { \tl_if_in:nnTF {#1} {=} { \seq_set_split:Nnn \l_tmpa_seq {=} {#1} } { \seq_set_split:Nnn \l_tmpa_seq {/} {#1} } \int_compare:nNnTF { \seq_count:N \l_tmpa_seq } = { 2 } { \tl_set:Nx \l_tmpa_tl { \seq_item:Nn \l_tmpa_seq {1} } \tl_set:Nx \l_tmpb_tl { \seq_item:Nn \l_tmpa_seq {2} } \exp_args:NVV \__integramme_resolve_ids:nnNN \l_tmpa_tl \l_tmpb_tl #2 #3 } { \int_zero:N #2 \int_zero:N #3 \msg_error:nnn { integramme } { bad-reference } {#1} \bool_set_false:N \l__integramme_valid_bool } } \cs_new:Npn \__integramme_mark_key:nnnn #1#2#3#4 { \int_compare:nNnTF {#1} < {#3} { \int_eval:n {#1}:\int_eval:n {#2}| \int_eval:n {#3}:\int_eval:n {#4} } { \int_eval:n {#3}:\int_eval:n {#4}| \int_eval:n {#1}:\int_eval:n {#2} } } \cs_new_protected:Npn \__integramme_register_resolved_mark:nnnnn #1#2#3#4#5 { \int_compare:nNnT {#2} > {0} { \int_compare:nNnT {#4} > {0} { \int_compare:nNnTF {#2} = {#4} { \msg_error:nnx { integramme } { same-category-mark } { \__integramme_cat_id:n {#2} } \bool_set_false:N \l__integramme_valid_bool } { \tl_set:Nx \l_tmpa_tl { \__integramme_mark_key:nnnn {#2}{#3}{#4}{#5} } \prop_get:NVNTF \l__integramme_manual_marks_prop \l_tmpa_tl \l_tmpb_tl { \tl_if_eq:NnF \l_tmpb_tl {#1} { \msg_error:nnx { integramme } { conflicting-mark } { \tl_use:N \l_tmpa_tl } \bool_set_false:N \l__integramme_valid_bool } } { \prop_put:NVn \l__integramme_manual_marks_prop \l_tmpa_tl {#1} } } } } } \cs_new_protected:Npn \__integramme_register_manual_mark:nnn #1#2#3 { \__integramme_resolve_ref:nNN {#2} \l__integramme_refa_cat_int \l__integramme_refa_item_int \__integramme_resolve_ref:nNN {#3} \l__integramme_refb_cat_int \l__integramme_refb_item_int \__integramme_register_resolved_mark:nnnnn {#1} {\l__integramme_refa_cat_int}{\l__integramme_refa_item_int} {\l__integramme_refb_cat_int}{\l__integramme_refb_item_int} } \cs_new_protected:Npn \__integramme_resolve_manual_marks: { \prop_clear:N \l__integramme_manual_marks_prop \seq_map_inline:Nn \l__integramme_manual_yes_seq { \__integramme_register_manual_mark:nnn {yes} ##1 } \seq_map_inline:Nn \l__integramme_manual_no_seq { \__integramme_register_manual_mark:nnn {no} ##1 } \bool_lazy_or:nnT { ! \seq_if_empty_p:N \l__integramme_manual_yes_seq } { ! \seq_if_empty_p:N \l__integramme_manual_no_seq } { \tl_if_eq:NnF \l__integramme_mode_tl { manual } { \msg_warning:nnx { integramme } { manual-marks-ignored } { \tl_use:N \l__integramme_mode_tl } } } } % -------------------------------------------------------------------------- % Validation % -------------------------------------------------------------------------- \prop_new:N \l__integramme_solution_seen_prop \cs_new_protected:Npn \__integramme_solution_row_bare:n #1 { \msg_error:nnn { integramme } { bad-keyed-solution-entry } {#1} \bool_set_false:N \l__integramme_valid_bool } \cs_new_protected:Npn \__integramme_solution_row_keyval:nn #1#2 { \tl_set:Nn \l_tmpa_tl {#1} \tl_set:Nn \l_tmpb_tl {#2} \tl_trim_spaces:N \l_tmpa_tl \tl_trim_spaces:N \l_tmpb_tl \prop_if_in:NVTF \l__integramme_titles_prop \l_tmpa_tl { \prop_if_in:NVTF \l__integramme_solution_row_prop \l_tmpa_tl { \msg_error:nnx { integramme } { duplicate-solution-category } { \tl_use:N \l_tmpa_tl } \bool_set_false:N \l__integramme_valid_bool } { \prop_put:NVV \l__integramme_solution_row_prop \l_tmpa_tl \l_tmpb_tl } } { \msg_error:nnx { integramme } { unknown-category } { \tl_use:N \l_tmpa_tl } \bool_set_false:N \l__integramme_valid_bool } } \cs_new_protected:Npn \__integramme_normalize_keyed_solution_row:n #1 { \prop_clear:N \l__integramme_solution_row_prop \keyval_parse:NNn \__integramme_solution_row_bare:n \__integramme_solution_row_keyval:nn {#1} \clist_clear:N \l_tmpa_clist \seq_map_inline:Nn \l__integramme_categories_seq { \prop_get:NnNTF \l__integramme_solution_row_prop {##1} \l_tmpa_tl { \clist_put_right:NV \l_tmpa_clist \l_tmpa_tl } { \msg_error:nnn { integramme } { missing-solution-category } {##1} \bool_set_false:N \l__integramme_valid_bool \clist_put_right:Nn \l_tmpa_clist { __integramme_missing__ } } } \tl_set:Nx \l_tmpa_tl { \clist_use:Nn \l_tmpa_clist {,} } \seq_put_right:NV \l__integramme_solution_seq \l_tmpa_tl } \cs_new_protected:Npn \__integramme_normalize_solution_inputs: { \seq_clear:N \l__integramme_solution_seq \seq_map_inline:Nn \l__integramme_solution_input_seq { \tl_if_in:nnTF {##1} {=} { \__integramme_normalize_keyed_solution_row:n {##1} } { \seq_put_right:Nn \l__integramme_solution_seq {##1} } } } \cs_new_protected:Npn \__integramme_validate_solution_row:n #1 { \clist_set:Nn \l_tmpa_clist {#1} \int_compare:nNnTF { \clist_count:N \l_tmpa_clist } = { \l__integramme_category_count_int } { \int_step_inline:nn { \l__integramme_category_count_int } { \tl_set:Nx \l_tmpa_tl { \clist_item:Nn \l_tmpa_clist {##1} } \tl_trim_spaces:N \l_tmpa_tl \tl_set:Nx \l_tmpb_tl { \__integramme_cat_id:n {##1} } \tl_if_eq:NnF \l_tmpa_tl { __integramme_missing__ } { \tl_set:Nx \l_tmpc_tl { \__integramme_item_key:nn { \tl_use:N \l_tmpb_tl } { \tl_use:N \l_tmpa_tl } } \prop_if_in:NVTF \l__integramme_item_index_prop \l_tmpc_tl { \tl_set:Nx \l_tmpd_tl { ##1 | \tl_use:N \l_tmpa_tl } \prop_if_in:NVTF \l__integramme_solution_seen_prop \l_tmpd_tl { \msg_error:nnxx { integramme } { duplicate-solution-item } { \tl_use:N \l_tmpb_tl } { \tl_use:N \l_tmpa_tl } \bool_set_false:N \l__integramme_valid_bool } { \prop_put:NVn \l__integramme_solution_seen_prop \l_tmpd_tl {1} } } { \msg_error:nnxx { integramme } { unknown-item } { \tl_use:N \l_tmpb_tl } { \tl_use:N \l_tmpa_tl } \bool_set_false:N \l__integramme_valid_bool } } } } { \msg_error:nnx { integramme } { bad-solution-row } { \int_use:N \l__integramme_category_count_int } \bool_set_false:N \l__integramme_valid_bool } } \cs_new_protected:Npn \__integramme_validate: { \int_set:Nn \l__integramme_category_count_int { \seq_count:N \l__integramme_categories_seq } % cell-size is used as a divisor by the layout code. Rejecting zero (and % negative values) here gives a package diagnostic instead of an opaque % floating-point/TikZ error later in the drawing stage. \dim_compare:nNnF { \l__integramme_cell_dim } > { 0pt } { \msg_error:nn { integramme } { nonpositive-cell-size } \bool_set_false:N \l__integramme_valid_bool } % Declaration-time errors (for example duplicate item identifiers) set the % same validity flag. Do not reset it here: that was a pre-0.7 bug which % could allow an invalid puzzle to reach the drawing stage. \bool_if:NT \l__integramme_valid_bool { \int_compare:nNnTF { \l__integramme_category_count_int } < { 2 } { \msg_error:nn { integramme } { too-few-categories } \bool_set_false:N \l__integramme_valid_bool } { \tl_set:Nx \l_tmpa_tl { \seq_item:Nn \l__integramme_categories_seq {1} } \prop_get:NVN \l__integramme_item_count_prop \l_tmpa_tl \l_tmpb_tl \int_set:Nn \l__integramme_order_int { \l_tmpb_tl } \seq_map_inline:Nn \l__integramme_categories_seq { \prop_get:NnN \l__integramme_item_count_prop {##1} \l_tmpb_tl \int_compare:nNnT { \l_tmpb_tl } = {0} { \msg_error:nnn { integramme } { empty-category } {##1} \bool_set_false:N \l__integramme_valid_bool } \int_compare:nNnF { \l_tmpb_tl } = { \l__integramme_order_int } { \msg_error:nnn { integramme } { unequal-sizes } {##1} \bool_set_false:N \l__integramme_valid_bool } } } } \bool_if:NT \l__integramme_valid_bool { \tl_if_eq:NnT \l__integramme_mode_tl { solution } { \seq_if_empty:NTF \l__integramme_solution_input_seq { \msg_error:nn { integramme } { missing-solution } \bool_set_false:N \l__integramme_valid_bool } { \__integramme_normalize_solution_inputs: \int_compare:nNnF { \seq_count:N \l__integramme_solution_seq } = { \l__integramme_order_int } { \msg_error:nnx { integramme } { bad-solution-size } { \int_use:N \l__integramme_order_int } \bool_set_false:N \l__integramme_valid_bool } \prop_clear:N \l__integramme_solution_seen_prop \seq_map_inline:Nn \l__integramme_solution_seq { \__integramme_validate_solution_row:n {##1} } } } } \bool_if:NT \l__integramme_valid_bool { \__integramme_resolve_manual_marks: } } % -------------------------------------------------------------------------- % Helpers % -------------------------------------------------------------------------- \cs_new:Npn \__integramme_cat_id:n #1 { \seq_item:Nn \l__integramme_categories_seq {#1} } \cs_new:Npn \__integramme_item_id:nn #1#2 { \prop_item:Ne \l__integramme_item_id_prop { \__integramme_index_key:nn { \__integramme_cat_id:n {#1} } {#2} } } \cs_new:Npn \__integramme_item:nn #1#2 { \prop_item:Ne \l__integramme_item_label_prop { \__integramme_index_key:nn { \__integramme_cat_id:n {#1} } {#2} } } \cs_new:Npn \__integramme_dim_units:n #1 { \fp_eval:n { \dim_to_fp:n {#1} / \dim_to_fp:n { \l__integramme_cell_dim } } } \prg_new_conditional:Npnn \__integramme_solution_match:nnnn #1#2#3#4 { T, F, TF } { \bool_set_false:N \l_tmpa_bool \seq_map_inline:Nn \l__integramme_solution_seq { \clist_set:Nn \l_tmpa_clist {##1} \tl_set:Nx \l_tmpa_tl { \clist_item:Nn \l_tmpa_clist {#1} } \tl_set:Nx \l_tmpb_tl { \clist_item:Nn \l_tmpa_clist {#3} } \tl_set:Nx \l_tmpc_tl { \__integramme_item_id:nn {#1}{#2} } \tl_set:Nx \l_tmpd_tl { \__integramme_item_id:nn {#3}{#4} } \tl_trim_spaces:N \l_tmpa_tl \tl_trim_spaces:N \l_tmpb_tl \tl_trim_spaces:N \l_tmpc_tl \tl_trim_spaces:N \l_tmpd_tl \tl_if_eq:NNT \l_tmpa_tl \l_tmpc_tl { \tl_if_eq:NNT \l_tmpb_tl \l_tmpd_tl { \bool_set_true:N \l_tmpa_bool \seq_map_break: } } } \bool_if:NTF \l_tmpa_bool { \prg_return_true: } { \prg_return_false: } } % -------------------------------------------------------------------------- % Automatic layout measurement % -------------------------------------------------------------------------- \cs_new_protected:Npn \__integramme_measure_layout: { % ----- Width of the row-label column (automatic when side-label-width=0pt) \dim_compare:nNnT { \l__integramme_side_label_dim } = { 0pt } { \dim_zero:N \l_tmpa_dim \int_step_inline:nn { \l__integramme_category_count_int - 1 } { \int_step_inline:nn { \l__integramme_order_int } { \hbox_set:Nn \l_tmpa_box { {\l__integramme_font_tl \__integramme_item:nn {##1}{####1}} } \dim_compare:nNnT { \box_wd:N \l_tmpa_box } > { \l_tmpa_dim } { \dim_set:Nn \l_tmpa_dim { \box_wd:N \l_tmpa_box } } } } \dim_set:Nn \l__integramme_side_label_dim { \l_tmpa_dim + 2\l__integramme_side_padding_dim } \dim_compare:nNnT { \l__integramme_side_label_dim } < { \l__integramme_side_label_min_dim } { \dim_set_eq:NN \l__integramme_side_label_dim \l__integramme_side_label_min_dim } } % ----- Decide whether all top labels must be slanted. % In auto mode, one label that does not fit its cell is enough to slant % the complete top header. This keeps the visual rhythm consistent. \tl_if_eq:NnTF \l__integramme_column_labels_tl { slanted } { \bool_set_true:N \l__integramme_top_slanted_bool } { \tl_if_eq:NnTF \l__integramme_column_labels_tl { horizontal } { \bool_set_false:N \l__integramme_top_slanted_bool } { \bool_set_false:N \l__integramme_top_slanted_bool \dim_set:Nn \l_tmpa_dim { \l__integramme_cell_dim - 2\l__integramme_top_padding_dim } \int_step_inline:nnnn {2}{1}{\l__integramme_category_count_int} { \int_step_inline:nn { \l__integramme_order_int } { \hbox_set:Nn \l_tmpa_box { {\l__integramme_font_tl \__integramme_item:nn {##1}{####1}} } \dim_compare:nNnT { \box_wd:N \l_tmpa_box } > { \l_tmpa_dim } { \bool_set_true:N \l__integramme_top_slanted_bool } } } } } % ----- Effective height of the short vertical stem used by slanted % separators. By default it is approximately one capital-letter height. \dim_compare:nNnTF { \l__integramme_guide_stem_dim } = { 0pt } { \hbox_set:Nn \l_tmpa_box { {\l__integramme_font_tl X} } \dim_set:Nn \l__integramme_guide_stem_effective_dim { \box_ht:N \l_tmpa_box + \box_dp:N \l_tmpa_box } } { \dim_set_eq:NN \l__integramme_guide_stem_effective_dim \l__integramme_guide_stem_dim } % ----- Required top-label height \dim_set_eq:NN \l__integramme_top_label_dim \l__integramme_top_label_min_dim \bool_if:NTF \l__integramme_top_slanted_bool { \int_step_inline:nnnn {2}{1}{\l__integramme_category_count_int} { \int_step_inline:nn { \l__integramme_order_int } { \hbox_set:Nn \l_tmpa_box { {\l__integramme_font_tl \__integramme_item:nn {##1}{####1}} } \fp_set:Nn \l_tmpa_fp { \dim_to_fp:n { \box_wd:N \l_tmpa_box } * sind(\l__integramme_top_angle_fp) + \dim_to_fp:n { \box_ht:N \l_tmpa_box + \box_dp:N \l_tmpa_box } * cosd(\l__integramme_top_angle_fp) } \dim_set:Nn \l_tmpb_dim { \fp_to_dim:n { \l_tmpa_fp } + 2\l__integramme_top_padding_dim } \dim_compare:nNnT { \l_tmpb_dim } > { \l__integramme_top_label_dim } { \dim_set_eq:NN \l__integramme_top_label_dim \l_tmpb_dim } } } % The bent guide itself must also fit inside the label area. \fp_set:Nn \l_tmpa_fp { \dim_to_fp:n { \l__integramme_guide_stem_effective_dim } + \l__integramme_guide_length_fp * \dim_to_fp:n { \l__integramme_cell_dim } * sind(\l__integramme_top_angle_fp) } \dim_set:Nn \l_tmpb_dim { \fp_to_dim:n { \l_tmpa_fp } + \l__integramme_top_padding_dim } \dim_compare:nNnT { \l_tmpb_dim } > { \l__integramme_top_label_dim } { \dim_set_eq:NN \l__integramme_top_label_dim \l_tmpb_dim } } { \dim_zero:N \l_tmpa_dim \int_step_inline:nnnn {2}{1}{\l__integramme_category_count_int} { \int_step_inline:nn { \l__integramme_order_int } { \hbox_set:Nn \l_tmpa_box { {\l__integramme_font_tl \__integramme_item:nn {##1}{####1}} } \dim_set:Nn \l_tmpb_dim { \box_ht:N \l_tmpa_box + \box_dp:N \l_tmpa_box } \dim_compare:nNnT { \l_tmpb_dim } > { \l_tmpa_dim } { \dim_set_eq:NN \l_tmpa_dim \l_tmpb_dim } } } \dim_set:Nn \l_tmpb_dim { \l_tmpa_dim + 2\l__integramme_top_padding_dim } \dim_compare:nNnT { \l_tmpb_dim } > { \l__integramme_top_label_dim } { \dim_set_eq:NN \l__integramme_top_label_dim \l_tmpb_dim } } } % -------------------------------------------------------------------------- % Drawing helpers % -------------------------------------------------------------------------- \cs_new_protected:Npn \__integramme_draw_mark:nnn #1#2#3 { \node[ font=\l__integramme_mark_font_tl, text=\l__integramme_text_color_tl ] at (#2,#3) { \str_if_eq:nnTF {#1} {yes} { \l__integramme_yes_tl } { \l__integramme_no_tl } }; } \cs_new_protected:Npn \__integramme_draw_pair_block:nn #1#2 { % #1 = row category index i % #2 = column category index j (j > i) \fp_set:Nn \l_tmpa_fp { ( \l__integramme_category_count_int - #2 ) * \l__integramme_order_int } \fp_set:Nn \l_tmpb_fp { - ( #1 - 1 ) * \l__integramme_order_int } % Thin inner grid. \int_step_inline:nn { \l__integramme_order_int - 1 } { \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_rule_dim ] ({\fp_use:N\l_tmpa_fp + ##1},{\fp_use:N\l_tmpb_fp}) -- ({\fp_use:N\l_tmpa_fp + ##1},{\fp_use:N\l_tmpb_fp - \l__integramme_order_int}); \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_rule_dim ] ({\fp_use:N\l_tmpa_fp},{\fp_use:N\l_tmpb_fp - ##1}) -- ({\fp_use:N\l_tmpa_fp + \l__integramme_order_int},{\fp_use:N\l_tmpb_fp - ##1}); } % Thick block border. \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_block_rule_dim ] ({\fp_use:N\l_tmpa_fp},{\fp_use:N\l_tmpb_fp}) rectangle ({\fp_use:N\l_tmpa_fp + \l__integramme_order_int}, {\fp_use:N\l_tmpb_fp - \l__integramme_order_int}); % Optional marks. \tl_if_eq:NnTF \l__integramme_mode_tl { solution } { \int_step_inline:nn { \l__integramme_order_int } { \int_set:Nn \l__integramme_draw_row_int {##1} \int_step_inline:nn { \l__integramme_order_int } { \__integramme_solution_match:nnnnTF {#1}{\l__integramme_draw_row_int}{#2}{####1} { \__integramme_draw_mark:nnn {yes} {\fp_eval:n { \l_tmpa_fp + ####1 - .5 }} {\fp_eval:n { \l_tmpb_fp - \l__integramme_draw_row_int + .5 }} } { \__integramme_draw_mark:nnn {no} {\fp_eval:n { \l_tmpa_fp + ####1 - .5 }} {\fp_eval:n { \l_tmpb_fp - \l__integramme_draw_row_int + .5 }} } } } } { \tl_if_eq:NnT \l__integramme_mode_tl { manual } { \int_step_inline:nn { \l__integramme_order_int } { \int_set:Nn \l__integramme_draw_row_int {##1} \int_step_inline:nn { \l__integramme_order_int } { \tl_set:Nx \l_tmpa_tl { \__integramme_mark_key:nnnn {#1}{\l__integramme_draw_row_int}{#2}{####1} } \prop_get:NVNT \l__integramme_manual_marks_prop \l_tmpa_tl \l_tmpb_tl { \tl_if_eq:NnTF \l_tmpb_tl {yes} { \__integramme_draw_mark:nnn {yes} {\fp_eval:n { \l_tmpa_fp + ####1 - .5 }} {\fp_eval:n { \l_tmpb_fp - \l__integramme_draw_row_int + .5 }} } { \__integramme_draw_mark:nnn {no} {\fp_eval:n { \l_tmpa_fp + ####1 - .5 }} {\fp_eval:n { \l_tmpb_fp - \l__integramme_draw_row_int + .5 }} } } } } } } } \cs_new_protected:Npn \__integramme_draw_side_labels: { \fp_set:Nn \l_tmpc_fp { \__integramme_dim_units:n { \l__integramme_side_label_dim } } \fp_set:Nn \l_tmpd_fp { \__integramme_dim_units:n { \l__integramme_side_title_dim } } \fp_set:Nn \l_tmpe_fp { \__integramme_dim_units:n { \l__integramme_side_padding_dim } } \int_step_inline:nn { \l__integramme_category_count_int - 1 } { \fp_set:Nn \l_tmpa_fp { - (##1 - 1) * \l__integramme_order_int } \tl_set:Nx \l_tmpa_tl { \__integramme_cat_id:n {##1} } \prop_get:NVN \l__integramme_titles_prop \l_tmpa_tl \l_tmpb_tl % Outer side block: thick border. \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_block_rule_dim ] ({-\fp_use:N\l_tmpc_fp-\fp_use:N\l_tmpd_fp},{\fp_use:N\l_tmpa_fp}) rectangle (0,{\fp_use:N\l_tmpa_fp-\l__integramme_order_int}); % Thin divider between the vertical category title and row labels. \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_rule_dim ] ({-\fp_use:N\l_tmpc_fp},{\fp_use:N\l_tmpa_fp}) -- ({-\fp_use:N\l_tmpc_fp},{\fp_use:N\l_tmpa_fp-\l__integramme_order_int}); % Thin row separators only through the item-label area. \int_step_inline:nn { \l__integramme_order_int - 1 } { \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_rule_dim ] ({-\fp_use:N\l_tmpc_fp},{\fp_use:N\l_tmpa_fp-####1}) -- (0,{\fp_use:N\l_tmpa_fp-####1}); } % Category title (vertical). \node[ font=\l__integramme_title_font_tl, text=\l__integramme_text_color_tl, rotate=90, anchor=center ] at ({-\fp_use:N\l_tmpc_fp-.5*\fp_use:N\l_tmpd_fp}, {\fp_use:N\l_tmpa_fp-.5*\l__integramme_order_int}) {\l_tmpb_tl}; % Row item labels. \int_step_inline:nn { \l__integramme_order_int } { \node[ font=\l__integramme_font_tl, text=\l__integramme_text_color_tl, anchor=west, inner~sep=0pt ] at ({-\fp_use:N\l_tmpc_fp+\fp_use:N\l_tmpe_fp}, {\fp_use:N\l_tmpa_fp-####1+.5}) {\__integramme_item:nn {##1}{####1}}; } } } \cs_new_protected:Npn \__integramme_draw_top_labels: { \fp_set:Nn \l_tmpc_fp { \__integramme_dim_units:n { \l__integramme_top_label_dim } } \fp_set:Nn \l_tmpd_fp { \__integramme_dim_units:n { \l__integramme_title_height_dim } } \fp_set:Nn \l_tmpe_fp { \__integramme_dim_units:n { \l__integramme_top_padding_dim } } \fp_set:Nn \l_tmph_fp { \__integramme_dim_units:n { \l__integramme_guide_stem_effective_dim } } % Categories are reversed on top: k, k-1, ..., 2. \int_step_inline:nnnn {\l__integramme_category_count_int}{-1}{2} { \fp_set:Nn \l_tmpa_fp { ( \l__integramme_category_count_int - ##1 ) * \l__integramme_order_int } \tl_set:Nx \l_tmpa_tl { \__integramme_cat_id:n {##1} } \prop_get:NVN \l__integramme_titles_prop \l_tmpa_tl \l_tmpb_tl % Complete header block with thick outline. \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_block_rule_dim ] ({\fp_use:N\l_tmpa_fp},0) rectangle ({\fp_use:N\l_tmpa_fp+\l__integramme_order_int}, {\fp_use:N\l_tmpc_fp+\fp_use:N\l_tmpd_fp}); % Thin separator below category title. \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_rule_dim ] ({\fp_use:N\l_tmpa_fp},{\fp_use:N\l_tmpc_fp}) -- ({\fp_use:N\l_tmpa_fp+\l__integramme_order_int},{\fp_use:N\l_tmpc_fp}); % Category title. \node[ font=\l__integramme_title_font_tl, text=\l__integramme_text_color_tl, align=center, text~width=\dim_eval:n { \l__integramme_order_int\l__integramme_cell_dim - 2\l__integramme_top_padding_dim }, inner~sep=0pt ] at ({\fp_use:N\l_tmpa_fp+.5*\l__integramme_order_int}, {\fp_use:N\l_tmpc_fp+.5*\fp_use:N\l_tmpd_fp}) {\l_tmpb_tl}; \bool_if:NTF \l__integramme_top_slanted_bool { % Diagonal guides, parallel to the text. \bool_if:NT \l__integramme_guides_bool { \int_step_inline:nn { \l__integramme_order_int } { \fp_set:Nn \l_tmpf_fp { \l__integramme_guide_length_fp * cosd(\l__integramme_top_angle_fp) } \fp_set:Nn \l_tmpg_fp { \l__integramme_guide_length_fp * sind(\l__integramme_top_angle_fp) } \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_rule_dim, opacity=\fp_use:N \l__integramme_guide_opacity_fp ] ({\fp_use:N\l_tmpa_fp+####1-1},0) -- ({\fp_use:N\l_tmpa_fp+####1-1},{\fp_use:N\l_tmph_fp}) -- ({\fp_use:N\l_tmpa_fp+####1-1+\fp_use:N\l_tmpf_fp}, {\fp_use:N\l_tmph_fp+\fp_use:N\l_tmpg_fp}); } } % Slanted labels are shifted half a cell right, so each label sits % midway between two consecutive guide/column lines. \int_step_inline:nn { \l__integramme_order_int } { \node[ font=\l__integramme_font_tl, text=\l__integramme_text_color_tl, rotate=\fp_use:N \l__integramme_top_angle_fp, anchor=south~west, inner~sep=0pt ] at ({\fp_use:N\l_tmpa_fp+####1-.5}, {\fp_use:N\l_tmpe_fp}) {\__integramme_item:nn {##1}{####1}}; } } { % Horizontal mode: ordinary vertical separators. \int_step_inline:nn { \l__integramme_order_int - 1 } { \draw[ draw=\l__integramme_grid_color_tl, line~width=\dim_use:N \l__integramme_rule_dim ] ({\fp_use:N\l_tmpa_fp+####1},0) -- ({\fp_use:N\l_tmpa_fp+####1},{\fp_use:N\l_tmpc_fp}); } \int_step_inline:nn { \l__integramme_order_int } { \node[ font=\l__integramme_font_tl, text=\l__integramme_text_color_tl, anchor=center, inner~sep=0pt ] at ({\fp_use:N\l_tmpa_fp+####1-.5},{.5*\fp_use:N\l_tmpc_fp}) {\__integramme_item:nn {##1}{####1}}; } } } } \cs_new_protected:Npn \__integramme_prepare_total_width: { \int_set:Nn \l_tmpa_int { ( \l__integramme_category_count_int - 1 ) * \l__integramme_order_int } \dim_set:Nn \l__integramme_total_width_dim { \l__integramme_side_title_dim + \l__integramme_side_label_dim + \l_tmpa_int \l__integramme_cell_dim } \bool_if:NT \l__integramme_overflow_warning_bool { \dim_compare:nNnT { \l__integramme_total_width_dim } > { \linewidth } { \msg_warning:nnxx { integramme } { grid-too-wide } { \dim_use:N \l__integramme_total_width_dim } { \dim_use:N \linewidth } } } } \cs_new_protected:Npn \__integramme_print_indices: { \tl_if_empty:NF \l__integramme_indices_tl { \par\vspace{\l__integramme_indices_sep_dim} \begin{minipage}{\dim_use:N \l__integramme_total_width_dim} \raggedright {\l__integramme_indices_font_tl \begin{enumerate} \setlength{\topsep}{0pt} \setlength{\partopsep}{0pt} \setlength{\parsep}{0pt} \setlength{\itemsep}{0.2\baselineskip} \l__integramme_indices_tl \end{enumerate} } \end{minipage} } } \cs_new_protected:Npn \__integramme_draw: { \__integramme_measure_layout: \__integramme_prepare_total_width: \begin{tikzpicture}[ x=\dim_use:N \l__integramme_cell_dim, y=\dim_use:N \l__integramme_cell_dim, baseline=(current~bounding~box.north) ] % Pairwise blocks: staircase layout. \int_step_inline:nn { \l__integramme_category_count_int - 1 } { \int_step_inline:nnnn {\l__integramme_category_count_int}{-1}{##1+1} { \__integramme_draw_pair_block:nn {##1}{####1} } } \__integramme_draw_side_labels: \__integramme_draw_top_labels: \end{tikzpicture} \__integramme_print_indices: } % -------------------------------------------------------------------------- % Environment % -------------------------------------------------------------------------- \NewDocumentEnvironment { integramme } { O{} } { \group_begin: \bool_set_true:N \l__integramme_valid_bool \seq_clear:N \l__integramme_categories_seq \prop_clear:N \l__integramme_titles_prop \prop_clear:N \l__integramme_item_count_prop \prop_clear:N \l__integramme_item_id_prop \prop_clear:N \l__integramme_item_label_prop \prop_clear:N \l__integramme_item_index_prop \seq_clear:N \l__integramme_solution_seq \seq_clear:N \l__integramme_solution_input_seq \seq_clear:N \l__integramme_manual_yes_seq \seq_clear:N \l__integramme_manual_no_seq \prop_clear:N \l__integramme_manual_marks_prop \tl_clear:N \l__integramme_indices_tl \keys_set:nn { integramme } {#1} \__integramme_apply_preset: } { \__integramme_validate: \bool_if:NT \l__integramme_valid_bool { \__integramme_draw: } \group_end: } \ExplSyntaxOff % % \end{macrocode} % % \Finale