\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{DartmouthExam}[2026/08/20 v1.3 Quiz Formatting Class with Rubrics]

% Rubrics are opt-in so existing documents keep their current appearance.
% Enable with \documentclass[rubric]{DartmouthExam} or from the command line via
% \PassOptionsToClass{rubric}{DartmouthExam} before \input{...}.
\newif\ifDartmouthExamRubric
\DartmouthExamRubricfalse
\DeclareOption{rubric}{\DartmouthExamRubrictrue}
\DeclareOption{norubric}{\DartmouthExamRubricfalse}
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}}
\ProcessOptions\relax

\LoadClass[11pt,letterpaper]{article}

% Preamble-level switches are also provided for generated documents/build systems.
\newcommand{\ShowRubric}{\DartmouthExamRubrictrue}
\newcommand{\HideRubric}{\DartmouthExamRubricfalse}

\RequirePackage[margin=1in]{geometry}
\RequirePackage{graphicx}
\RequirePackage{xparse}
\RequirePackage{xkeyval}
\RequirePackage{enumitem}
\RequirePackage{needspace}
\RequirePackage{amsfonts}
\RequirePackage{textcomp}
\RequirePackage{amsmath}
\RequirePackage{etoolbox}
\RequirePackage{pgffor}
\RequirePackage{calc}
\RequirePackage{changepage}

\newcommand{\@coursename}{}
\newcommand{\@quiztitle}{}
\newcommand{\@quizdate}{}
\newcommand{\@professor}{}
\newcommand{\@profemail}{}
\newcommand{\@profoffice}{}
\newcommand{\@profpronouns}{}
\newcommand{\@instructions}{}
\newcommand{\@term}{}
\newcommand{\@department}{}
\newcommand{\@schoolname}{Dartmouth College}

\newcommand{\CourseName}[1]{\renewcommand{\@coursename}{#1}}
\newcommand{\QuizTitle}[1]{\renewcommand{\@quiztitle}{#1}}
\newcommand{\DateAdministered}[1]{\renewcommand{\@quizdate}{#1}}
\newcommand{\Professor}[1]{\renewcommand{\@professor}{#1}}
\newcommand{\ProfessorEmail}[1]{\renewcommand{\@profemail}{#1}}
\newcommand{\ProfessorOfficeLocation}[1]{\renewcommand{\@profoffice}{#1}}
\newcommand{\ProfessorPronouns}[1]{\renewcommand{\@profpronouns}{#1}}
\newcommand{\Instructions}[1]{\renewcommand{\@instructions}{#1}}
\newcommand{\Term}[1]{\renewcommand{\@term}{#1}}
\newcommand{\Department}[1]{\renewcommand{\@department}{#1}}
\newcommand{\SchoolName}[1]{\renewcommand{\@schoolname}{#1}}

% Points summary command
% Points summary command - formatted with aligned brackets and slashes
% Custom category system
\RequirePackage{xstring}
\newcommand{\@categorylist}{}
\newcommand{\@currentcategory}{}

% Command to create new question categories
\newcommand{\NewQuestionCategory}[1]{%
    \expandafter\newcounter{#1points}%
    \expandafter\regtotcounter{#1points}%
    \ifx\@categorylist\empty
        \xdef\@categorylist{#1}%
    \else
        \xdef\@categorylist{\@categorylist,#1}%
    \fi
}

% Helper to render category-based points dynamically
\newcommand{\@rendercategorypoints}{%
    % Build rows dynamically from category list
    \ifx\@categorylist\empty
        % No categories defined
    \else
        \expandafter\@processcategorylist\expandafter{\@categorylist}%
    \fi
}

% Process the comma-separated category list
\newcommand{\@processcategorylist}[1]{%
    \@for\@category:=#1\do{%
        \expandafter\@rendercategoryrow\expandafter{\@category}%
    }%
}

% Render a single category row
\newcommand{\@rendercategoryrow}[1]{%
    \@ifundefined{c@#1points}{}{%
        #1 & [ \hspace{1.5cm} & / & \arabic{#1points} ] \\
    }%
}

% Helper to render question type-based points
\newcommand{\@renderquestiontypepoints}{%
    % Show all question types (will show 0 if no questions of that type)
    Multiple Choice & [ \hspace{1.5cm} & / & \arabic{mcpoints} ] \\
    Short Answer & [ \hspace{1.5cm} & / & \arabic{shortanswerpoints} ] \\
    True/False & [ \hspace{1.5cm} & / & \arabic{tfpoints} ] \\
    Fill in Blank & [ \hspace{1.5cm} & / & \arabic{fillblankpoints} ] \\
    Essay & [ \hspace{1.5cm} & / & \arabic{essaypoints} ] \\
    Numerical & [ \hspace{1.5cm} & / & \arabic{numericalpoints} ] \\
    Matching & [ \hspace{1.5cm} & / & \arabic{matchingpoints} ] \\
    Classification & [ \hspace{1.5cm} & / & \arabic{classificationpoints} ] \\
}

% New PointsTotal command with optional category support
\NewDocumentCommand{\PointsTotal}{O{}}{%
  \begin{flushright}
  \begin{tabular}{l@{\hspace{1em}}r@{}c@{}l}
    \IfStrEq{#1}{Categories}{%
        \@rendercategorypoints
    }{%
        \@renderquestiontypepoints
    }%
    \textbf{Total} & \textbf{[ \hspace{1.5cm}} & \textbf{/} & \textbf{\arabic{totalpoints} ]} \\
  \end{tabular}
  \end{flushright}
  \vspace{5mm}
}

% Keep old makepoints for backward compatibility (deprecated)
\newcommand{\makepoints}{\PointsTotal}

% Simple approach: use totvalue package to get counter values
\RequirePackage{totcount}

% Register counters for totaling
\regtotcounter{mcpoints}
\regtotcounter{shortanswerpoints}
\regtotcounter{tfpoints}
\regtotcounter{fillblankpoints}
\regtotcounter{essaypoints}
\regtotcounter{numericalpoints}
\regtotcounter{matchingpoints}
\regtotcounter{classificationpoints}
\regtotcounter{totalpoints}

\newcommand{\makeheader}{%
  \noindent
  % Three-column header layout
  \begin{minipage}[c]{0.15\textwidth}
    \centering
    \includegraphics[width=\linewidth,height=3cm,keepaspectratio]{DPineBlack.png}
  \end{minipage}%
  \hfill
  \begin{minipage}[c]{0.45\textwidth}
    \raggedright
    \Large\bfseries \@quiztitle \\
    \vspace{2mm}
    \normalsize \@coursename \\
    \@term \\
    \@department \\
    \@schoolname
  \end{minipage}%
  \hfill
  \begin{minipage}[c]{0.35\textwidth}
    \raggedleft
    \@professor \\
    \@profpronouns \\
    \@profemail \\
    \@profoffice
  \end{minipage}
  
  \vspace{3mm}
  \hrule
  \vspace{2mm}
  
  \noindent
  \begin{tabular*}{\textwidth}{l @{\extracolsep{\fill}} r}
    \textbf{Date:} \@quizdate & \textbf{TOTAL: \quad [ \hspace{1.5cm} ]} \\
  \end{tabular*}
  \vspace{3mm}

  \noindent
  \small{\@instructions}
  \vspace{5mm}
}

\newcounter{questionnum}
\newcounter{subquestionnum}[questionnum]  % Reset subquestion counter when questionnum changes
\renewcommand{\thesubquestionnum}{\thequestionnum\alph{subquestionnum}}

% Question numbering mode: 'main' for top-level, 'sub' for sub-questions
\newcommand{\@questionmode}{main}
\newcommand{\setquestionmode}[1]{\renewcommand{\@questionmode}{#1}}

% Commands to start main questions and sub-questions
\newcommand{\MainQuestion}{\setquestionmode{main}}
\newcommand{\SubQuestion}{\setquestionmode{sub}}

% Question block environment for indentation
\NewDocumentEnvironment{questionblock}{}{%
    \ifdefstring{\@questionmode}{sub}{\begin{adjustwidth}{1cm}{0pt}}{}%
}{%
    \ifdefstring{\@questionmode}{sub}{\end{adjustwidth}}{}%
}



% Informational main question (no points, just context/setup)
\NewDocumentCommand{\InformationalQuestion}{O{} m}{%
    \refstepcounter{questionnum}%
    \setcounter{subquestionnum}{0}%  % Reset sub-question counter
    \begin{questionenv}[#1]
        \par\noindent
        \begin{minipage}[t]{\textwidth}
            \textbf{\thequestionnum.} #2
        \end{minipage}%
        \par\nopagebreak
    \end{questionenv}
    \vspace{2mm}
}

% Point tracking counters
\newcounter{mcpoints}
\newcounter{essaypoints}
\newcounter{shortanswerpoints}
\newcounter{tfpoints}
\newcounter{fillblankpoints}
\newcounter{numericalpoints}
\newcounter{matchingpoints}
\newcounter{classificationpoints}
\newcounter{totalpoints}


% -----------------------------------------------------------------------------
% Rubric support
% -----------------------------------------------------------------------------
% Rubric data is stored in two forms:
%   * automatic entries are captured while a question is typeset;
%   * manual entries are declared in a rubric/rubricEntry block and are written
%     to the .aux file so they can be printed beside an earlier question on the
%     next LaTeX pass. latexmk handles this rerun automatically.
%
% The rubric block is therefore a declaration section, not a second printed
% rubric. In rubric mode the entries are rendered immediately after the matching
% question so graders do not need to flip to another part of the document.

\newif\ifDartmouth@insidequestion
\Dartmouth@insidequestionfalse
\newif\ifDartmouth@inrubric
\Dartmouth@inrubricfalse
\newif\ifDartmouth@inrubricentry
\Dartmouth@inrubricentryfalse
\newcount\Dartmouth@rubricitemcount
\newcommand{\Dartmouth@currentrubrickey}{}
\newcommand{\Dartmouth@currentquestiondisplay}{}
\newcommand{\Dartmouth@questionkeylist}{}
\newcommand{\Dartmouth@manualtargetlist}{}
\newcommand{\Dartmouth@rubricentrylabel}{}

% Append stored rubric content without expanding its contents. Three rubric
% record types are supported:
%   * item: additive criterion with a point value;
%   * alternative: mutually exclusive full/partial-credit outcomes;
%   * answer: non-scoring model-answer material, which may contain arbitrary
%     LaTeX including display environments such as align.
% #1 = storage prefix, #2 = key/label.
\newcommand{\Dartmouth@ensurerubricstorage}[2]{%
    \@ifundefined{Dartmouth@rubric@#1@#2}{%
        \expandafter\gdef\csname Dartmouth@rubric@#1@#2\endcsname{}%
    }{}%
}

\newcommand{\Dartmouth@appendrubricitem}[4]{%
    \Dartmouth@ensurerubricstorage{#1}{#2}%
    \expandafter\g@addto@macro\csname Dartmouth@rubric@#1@#2\endcsname{%
        \Dartmouth@storedrubricitem{#3}{#4}%
    }%
}

\newcommand{\Dartmouth@appendrubricalternative}[6]{%
    \Dartmouth@ensurerubricstorage{#1}{#2}%
    \expandafter\g@addto@macro\csname Dartmouth@rubric@#1@#2\endcsname{%
        \Dartmouth@storedrubricalternative{#3}{#4}{#5}{#6}%
    }%
}

\newcommand{\Dartmouth@appendrubricanswer}[3]{%
    \Dartmouth@ensurerubricstorage{#1}{#2}%
    \expandafter\g@addto@macro\csname Dartmouth@rubric@#1@#2\endcsname{%
        \Dartmouth@storedrubricanswer{#3}%
    }%
}

% Called while the .aux file is read. These are intentionally public-looking
% internal commands because their names are serialized into the auxiliary file.
\newcommand{\DartmouthRubricAuxItem}[3]{%
    \Dartmouth@appendrubricitem{loadedmanual}{#1}{#2}{#3}%
}
\newcommand{\DartmouthRubricAuxAlternative}[5]{%
    \Dartmouth@appendrubricalternative{loadedmanual}{#1}{#2}{#3}{#4}{#5}%
}
\newcommand{\DartmouthRubricAuxAnswer}[2]{%
    \Dartmouth@appendrubricanswer{loadedmanual}{#1}{#2}%
}

% Initialize bookkeeping after the relevant question counter has been stepped.
\newcommand{\Dartmouth@beginquestionrubric}[2]{%
    \xdef\Dartmouth@currentrubrickey{#1}%
    \xdef\Dartmouth@currentquestiondisplay{#2}%
    \expandafter\gdef\csname Dartmouth@rubric@auto@#1\endcsname{}%
    \expandafter\gdef\csname Dartmouth@rubric@labels@#1\endcsname{}%
    \expandafter\xdef\csname Dartmouth@rubric@expected@#1\endcsname{\Dartmouth@questionpointsvalue}%
    \ifx\Dartmouth@questionkeylist\empty
        \xdef\Dartmouth@questionkeylist{#1}%
    \else
        \xdef\Dartmouth@questionkeylist{\Dartmouth@questionkeylist,#1}%
    \fi
}

% Save the point value before \Dartmouth@beginquestionrubric so the helper can
% define it by expansion without exposing another argument in its call sites.
\newcommand{\Dartmouth@questionpointsvalue}{0}

% Record every standard \label encountered in the question text. The original
% \label still runs, so ordinary \ref/\pageref semantics are preserved.
\newcommand{\Dartmouth@recordquestionlabel}[1]{%
    \expandafter\xdef\csname Dartmouth@rubric@labelkey@#1\endcsname{\Dartmouth@currentrubrickey}%
    \expandafter\xdef\csname Dartmouth@rubric@labelpoints@#1\endcsname{%
        \csname Dartmouth@rubric@expected@\Dartmouth@currentrubrickey\endcsname
    }%
    \edef\Dartmouth@labelstoragename{Dartmouth@rubric@labels@\Dartmouth@currentrubrickey}%
    \expandafter\g@addto@macro\csname\Dartmouth@labelstoragename\endcsname{%
        \Dartmouth@storedquestionlabel{#1}%
    }%
}

% Register one automatic item against the question currently being typeset.
\newcommand{\Dartmouth@addautomaticrubricitem}[2]{%
    \ifDartmouth@insidequestion
        \Dartmouth@appendrubricitem{auto}{\Dartmouth@currentrubrickey}{#1}{#2}%
    \else
        \PackageWarning{DartmouthExam}{Automatic rubric item used outside a question; item ignored}%
    \fi
}

% A key concept is visually neutral in the student copy, but it becomes a rubric
% item for the enclosing question.
\NewDocumentCommand{\keyconcept}{m m}{%
    \ifDartmouth@insidequestion
        \Dartmouth@addautomaticrubricitem{#1}{#2}%
    \else
        \PackageWarning{DartmouthExam}{\string\keyconcept\space used outside a question; no rubric item recorded}%
    \fi
    #1%
}

% The preferred inline syntax is executable rubric markup, for example:
%   \ShortAnswerQuestion{...}{2}{%
%     \rubricAnswer{...}%
%     \rubricItem{First criterion, commas are fine}{1}%
%     \rubricAlternative{Correct answer}{1}{Close answer}{0.5}%
%   }
%
% A compact delimiter syntax is also supported. It uses | between description
% and points and ; between entries, so commas are ordinary text:
%   {{First criterion, with comma|1};{Second criterion|1}}
%
% For backward compatibility, the v1.2 comma syntax remains accepted:
%   {{First criterion,1},{Second criterion,1}}
\ExplSyntaxOn
\cs_generate_variant:Nn \str_if_in:nnTF { ee }
\cs_new_protected:Npn \DartmouthProcessLegacyRubricList #1
  {
    \clist_map_inline:nn {#1}
      {
        \clist_set:Nn \l_tmpa_clist {##1}
        \int_compare:nNnTF { \clist_count:N \l_tmpa_clist } = {2}
          {
            \exp_args:Nxx \Dartmouth@addautomaticrubricitem
              { \clist_item:Nn \l_tmpa_clist {1} }
              { \clist_item:Nn \l_tmpa_clist {2} }
          }
          {
            \PackageError{DartmouthExam}
              {Malformed legacy automatic rubric item}
              {Each legacy item must have exactly two comma-separated fields: {description,points}.^^J
               Prefer the new syntax \string\rubricItem{description}{points} or {description|points}.}
          }
      }
  }

\cs_new_protected:Npn \DartmouthProcessInlineRubric #1
  {
    % If one of the explicit rubric commands occurs, the argument is a rubric
    % body and is executed directly. Otherwise choose the new compact parser
    % when a | delimiter is present, falling back to the legacy comma parser.
    \tl_if_in:nnTF {#1} {\rubricItem}
      {#1}
      {
        \tl_if_in:nnTF {#1} {\rubricAnswer}
          {#1}
          {
            \tl_if_in:nnTF {#1} {\rubricAlternative}
              {#1}
              {
                \str_if_in:eeTF { \tl_to_str:n {#1} } {|}
                  {\DartmouthProcessPipeRubricList{#1}}
                  {\DartmouthProcessLegacyRubricList{#1}}
              }
          }
      }
  }

\fp_new:N \l__dartmouth_rubric_sum_fp
\bool_new:N \l__dartmouth_rubric_has_items_bool
\cs_new_protected:Npn \DartmouthRubricValidationReset
  {
    \fp_zero:N \l__dartmouth_rubric_sum_fp
    \bool_set_false:N \l__dartmouth_rubric_has_items_bool
  }
\cs_new_protected:Npn \DartmouthRubricValidationAdd #1
  {
    \fp_add:Nn \l__dartmouth_rubric_sum_fp {#1}
    \bool_set_true:N \l__dartmouth_rubric_has_items_bool
  }
\cs_new_protected:Npn \DartmouthRubricValidationAddAlternative #1#2
  {
    % Alternative outcomes are mutually exclusive; only the highest score in
    % the outcome pair contributes to the question's maximum possible score.
    \fp_add:Nn \l__dartmouth_rubric_sum_fp { max((#1),(#2)) }
    \bool_set_true:N \l__dartmouth_rubric_has_items_bool
  }
\cs_new:Npn \DartmouthRubricValidationTotal
  { \fp_to_decimal:N \l__dartmouth_rubric_sum_fp }
\cs_new_protected:Npn \DartmouthRubricValidationIfHasItemsTF #1#2
  { \bool_if:NTF \l__dartmouth_rubric_has_items_bool {#1}{#2} }
\cs_new_protected:Npn \DartmouthRubricValidationCompare #1#2
  {
    \fp_compare:nNnF
      { abs(\l__dartmouth_rubric_sum_fp - (#1)) } < { 0.0000001 }
      {
        \PackageError{DartmouthExam}
          {Rubric~points~for~question~#2~total~\fp_to_decimal:N \l__dartmouth_rubric_sum_fp,
           ~but~the~question~is~worth~#1~points}
          {Adjust the rubric items so their points add up to the question total.}
      }
  }
\ExplSyntaxOff

% xparse's SplitList/SplitArgument respect brace groups. Consequently commas,
% semicolons, and other punctuation may appear safely inside a braced rubric
% description while | and ; remain convenient compact separators at top level.
\NewDocumentCommand{\DartmouthProcessPipeRubricList}{>{\SplitList{;}}m}{%
    \ProcessList{#1}{\DartmouthProcessPipeRubricItem}%
}
\NewDocumentCommand{\DartmouthProcessPipeRubricItem}{>{\SplitArgument{1}{|}}m}{%
    \DartmouthProcessPipeRubricItemAux#1%
}
\NewDocumentCommand{\DartmouthProcessPipeRubricItemAux}{m m}{%
    \IfNoValueTF{#2}{%
        \PackageError{DartmouthExam}{Malformed compact automatic rubric item}
            {Each compact item must be written as {description|points}, and entries are separated by semicolons.}%
    }{%
        \Dartmouth@addautomaticrubricitem{#1}{#2}%
    }%
}

% Manual rubric declaration syntax.
\NewDocumentEnvironment{rubric}{}{%
    \Dartmouth@inrubrictrue
}{%
    \Dartmouth@inrubricfalse
}

\NewDocumentEnvironment{rubricEntry}{m}{%
    \ifDartmouth@inrubric\else
        \PackageError{DartmouthExam}{rubricEntry must appear inside a rubric environment}
            {Wrap rubricEntry environments in \string\begin{rubric} ... \string\end{rubric}.}%
    \fi
    \Dartmouth@inrubricentrytrue
    \def\Dartmouth@rubricentrylabel{#1}%
    \@ifundefined{Dartmouth@rubric@manualcurrent@#1}{%
        \expandafter\gdef\csname Dartmouth@rubric@manualcurrent@#1\endcsname{}%
    }{}%
    \ifx\Dartmouth@manualtargetlist\empty
        \xdef\Dartmouth@manualtargetlist{#1}%
    \else
        \xdef\Dartmouth@manualtargetlist{\Dartmouth@manualtargetlist,#1}%
    \fi
}{%
    \Dartmouth@inrubricentryfalse
    \def\Dartmouth@rubricentrylabel{}%
}

\NewDocumentCommand{\rubricItem}{+m m}{%
    \ifDartmouth@insidequestion
        \Dartmouth@addautomaticrubricitem{#1}{#2}%
    \else
        \ifDartmouth@inrubricentry
            \Dartmouth@appendrubricitem{manualcurrent}{\Dartmouth@rubricentrylabel}{#1}{#2}%
            \protected@write\@auxout{}{%
                \string\DartmouthRubricAuxItem{\Dartmouth@rubricentrylabel}{\unexpanded{#1}}{\unexpanded{#2}}%
            }%
        \else
            \PackageError{DartmouthExam}{\string\rubricItem\space must be used in a question rubric or rubricEntry}
                {Place it in a question's inline rubric argument or inside \string\begin{rubricEntry}...\string\end{rubricEntry}.}%
        \fi
    \fi
}

% Mutually exclusive scoring outcomes. Validation counts max(#2,#4), not their
% sum. This is useful for rules such as "correct answer = 1 point; plausible but
% wrong answer = 0.5 points".
\NewDocumentCommand{\rubricAlternative}{+m m +m m}{%
    \ifDartmouth@insidequestion
        \Dartmouth@appendrubricalternative{auto}{\Dartmouth@currentrubrickey}{#1}{#2}{#3}{#4}%
    \else
        \ifDartmouth@inrubricentry
            \Dartmouth@appendrubricalternative{manualcurrent}{\Dartmouth@rubricentrylabel}{#1}{#2}{#3}{#4}%
            \protected@write\@auxout{}{%
                \string\DartmouthRubricAuxAlternative{\Dartmouth@rubricentrylabel}%
                    {\unexpanded{#1}}{\unexpanded{#2}}{\unexpanded{#3}}{\unexpanded{#4}}%
            }%
        \else
            \PackageError{DartmouthExam}{\string\rubricAlternative\space must be used in a question rubric or rubricEntry}
                {Place it in a question's inline rubric argument or inside \string\begin{rubricEntry}...\string\end{rubricEntry}.}%
        \fi
    \fi
}

% Non-scoring model-answer material. Because this is stored as a single braced
% argument instead of being parsed as a list field, it can contain arbitrary
% punctuation and display environments such as align/aligned/gather.
\NewDocumentCommand{\rubricAnswer}{+m}{%
    \ifDartmouth@insidequestion
        \Dartmouth@appendrubricanswer{auto}{\Dartmouth@currentrubrickey}{#1}%
    \else
        \ifDartmouth@inrubricentry
            \Dartmouth@appendrubricanswer{manualcurrent}{\Dartmouth@rubricentrylabel}{#1}%
            \protected@write\@auxout{}{%
                \string\DartmouthRubricAuxAnswer{\Dartmouth@rubricentrylabel}{\unexpanded{#1}}%
            }%
        \else
            \PackageError{DartmouthExam}{\string\rubricAnswer\space must be used in a question rubric or rubricEntry}
                {Place it in a question's inline rubric argument or inside \string\begin{rubricEntry}...\string\end{rubricEntry}.}%
        \fi
    \fi
}

% Execute manual entries associated with all labels attached to a question.
\newcommand{\Dartmouth@executeLoadedManualForLabel}[1]{%
    \@ifundefined{Dartmouth@rubric@loadedmanual@#1}{}{%
        \csname Dartmouth@rubric@loadedmanual@#1\endcsname
    }%
}
\newcommand{\Dartmouth@executeDisplayManualForLabel}[1]{%
    \@ifundefined{Dartmouth@rubric@manualcurrent@#1}{%
        \Dartmouth@executeLoadedManualForLabel{#1}%
    }{%
        \csname Dartmouth@rubric@manualcurrent@#1\endcsname
    }%
}
\newcommand{\Dartmouth@executeCurrentManualForLabel}[1]{%
    \@ifundefined{Dartmouth@rubric@manualcurrent@#1}{}{%
        \csname Dartmouth@rubric@manualcurrent@#1\endcsname
    }%
}

% Count rows before opening the visual block, allowing questions with no rubric
% to remain byte-for-byte close to their legacy layout.
\newcommand{\Dartmouth@countrubricrows}[1]{%
    \Dartmouth@rubricitemcount=0\relax
    \long\def\Dartmouth@storedrubricitem##1##2{\advance\Dartmouth@rubricitemcount by 1\relax}%
    \long\def\Dartmouth@storedrubricalternative##1##2##3##4{\advance\Dartmouth@rubricitemcount by 2\relax}%
    \long\def\Dartmouth@storedrubricanswer##1{\advance\Dartmouth@rubricitemcount by 1\relax}%
    \def\Dartmouth@storedquestionlabel##1{\Dartmouth@executeDisplayManualForLabel{##1}}%
    \csname Dartmouth@rubric@auto@#1\endcsname
    \csname Dartmouth@rubric@labels@#1\endcsname
}

\newcommand{\Dartmouth@printrubricpointrow}[3]{%
    \par\noindent
    \begin{minipage}[t]{\dimexpr\linewidth-2.2cm\relax}%
        #1#2%
    \end{minipage}%
    \hfill
    \begin{minipage}[t]{2cm}\raggedleft\textbf{#3 pts}\end{minipage}%
    \par
}

\newcommand{\Dartmouth@printrubricrows}[1]{%
    \long\def\Dartmouth@storedrubricitem##1##2{%
        \Dartmouth@printrubricpointrow{}{##1}{##2}%
    }%
    \long\def\Dartmouth@storedrubricalternative##1##2##3##4{%
        \par\smallskip
        \Dartmouth@printrubricpointrow{\textit{Full credit:}~}{##1}{##2}%
        \Dartmouth@printrubricpointrow{\textit{Partial/alternative:}~}{##3}{##4}%
        \smallskip
    }%
    \long\def\Dartmouth@storedrubricanswer##1{%
        \par\smallskip
        \noindent\textbf{Model answer:}\par\nobreak
        \begingroup
            \setlength{\abovedisplayskip}{4pt}%
            \setlength{\belowdisplayskip}{4pt}%
            ##1\par
        \endgroup
        \smallskip
    }%
    \def\Dartmouth@storedquestionlabel##1{\Dartmouth@executeDisplayManualForLabel{##1}}%
    \csname Dartmouth@rubric@auto@#1\endcsname
    \csname Dartmouth@rubric@labels@#1\endcsname
}

\newcommand{\Dartmouth@rendercurrentrubric}{%
    \ifDartmouthExamRubric
        \Dartmouth@countrubricrows{\Dartmouth@currentrubrickey}%
        \ifnum\Dartmouth@rubricitemcount>0\relax
            \Needspace{4\baselineskip}%
            \par\smallskip
            \noindent\begin{minipage}{\linewidth}
                \small
                \hrule\vspace{1.5mm}
                \noindent\textbf{Rubric -- Question \Dartmouth@currentquestiondisplay}%
                \hfill
                \textbf{\csname Dartmouth@rubric@expected@\Dartmouth@currentrubrickey\endcsname\ pts total}\par
                \vspace{1mm}
                \Dartmouth@printrubricrows{\Dartmouth@currentrubrickey}%
                \vspace{1mm}\hrule
            \end{minipage}
            \par\smallskip
        \fi
    \fi
}

% Validate current-source rubric declarations against each question's point
% value. Loaded .aux entries are deliberately not used here, so stale auxiliary
% data can never make an invalid current rubric pass validation.
\newcommand{\Dartmouth@validatequestionrubric}[1]{%
    \DartmouthRubricValidationReset
    \long\def\Dartmouth@storedrubricitem##1##2{\DartmouthRubricValidationAdd{##2}}%
    \long\def\Dartmouth@storedrubricalternative##1##2##3##4{\DartmouthRubricValidationAddAlternative{##2}{##4}}%
    \long\def\Dartmouth@storedrubricanswer##1{}%
    \def\Dartmouth@storedquestionlabel##1{\Dartmouth@executeCurrentManualForLabel{##1}}%
    \csname Dartmouth@rubric@auto@#1\endcsname
    \csname Dartmouth@rubric@labels@#1\endcsname
    \DartmouthRubricValidationIfHasItemsTF{%
        \DartmouthRubricValidationCompare
            {\csname Dartmouth@rubric@expected@#1\endcsname}
            {\csname Dartmouth@rubric@display@#1\endcsname}%
    }{}%
}

\newcommand{\Dartmouth@validatemanualtarget}[1]{%
    \@ifundefined{Dartmouth@rubric@labelkey@#1}{%
        \PackageError{DartmouthExam}
            {Rubric entry targets unknown question label `#1'}
            {Place \string\label{#1} inside the text of the intended question.}%
    }{}%
}

\AtEndDocument{%
    \ifDartmouthExamRubric
        \@for\Dartmouth@validationkey:=\Dartmouth@questionkeylist\do{%
            \ifx\Dartmouth@validationkey\empty\else
                \Dartmouth@validatequestionrubric{\Dartmouth@validationkey}%
            \fi
        }%
        \@for\Dartmouth@validationlabel:=\Dartmouth@manualtargetlist\do{%
            \ifx\Dartmouth@validationlabel\empty\else
                \Dartmouth@validatemanualtarget{\Dartmouth@validationlabel}%
            \fi
        }%
    \fi
}

\define@key{question}{AssociatedFigure}{\def\@questionFigure{#1}}
\define@key{question}{AssociatedFigureSide}{\def\@questionFigureSide{#1}}
\define@key{question}{FigureWidth}{\def\@figureWidth{#1}}
\define@key{question}{Category}{\def\@questionCategory{#1}}

\newcommand{\resetquestionkeys}{%
    \def\@questionFigure{}%
    \def\@questionFigureSide{right}%
    \def\@figureWidth{0.4\textwidth}%
    \def\@questionCategory{}%
}
\resetquestionkeys

% Helper function to add points to appropriate counter
\newcommand{\@addquestionpoints}[2]{%
    % #1 = question type counter name (e.g., mcpoints)
    % #2 = points value
    \addtocounter{totalpoints}{#2}%
    \ifx\@questionCategory\empty
        % No category specified, use question type counter
        \addtocounter{#1}{#2}%
    \else
        % Category specified, use category counter
        \expandafter\@addtocategorypoints\expandafter{\@questionCategory}{#2}%
    \fi
}

% Helper to add points to a specific category
\newcommand{\@addtocategorypoints}[2]{%
    \addtocounter{#1points}{#2}%
}

\newcommand{\placefigure}{%
    \ifx\@questionFigure\empty\else
        \begin{minipage}[c]{\@figureWidth}
            \centering
            \includegraphics[width=\linewidth]{\@questionFigure}
        \end{minipage}%
    \fi
}

\newcommand{\typesetquestion}[3]{%
    \def\Dartmouth@questionpointsvalue{#3}%
    \ifdefstring{\@questionmode}{main}{%
        \refstepcounter{questionnum}%
        \setcounter{subquestionnum}{0}%  % Reset sub-question counter
        \def\@currentquestionlabel{\thequestionnum.}%
        \def\@pointboxoffset{0pt}%
        \def\@contentindent{0pt}% No content indentation for main questions
        \edef\Dartmouth@newrubrickey{main-\arabic{questionnum}}%
        \edef\Dartmouth@newquestiondisplay{\thequestionnum}%
    }{%
        \refstepcounter{subquestionnum}%
        \def\@currentquestionlabel{\thequestionnum\alph{subquestionnum}.}%
        \def\@pointboxoffset{1cm}% Offset for sub-questions
        \def\@contentindent{1cm}% Content should align with sub-question text
        \edef\Dartmouth@newrubrickey{sub-\arabic{questionnum}-\alph{subquestionnum}}%
        \edef\Dartmouth@newquestiondisplay{\thequestionnum\alph{subquestionnum}}%
    }%
    \Dartmouth@beginquestionrubric{\Dartmouth@newrubrickey}{\Dartmouth@newquestiondisplay}%
    \expandafter\xdef\csname Dartmouth@rubric@display@\Dartmouth@currentrubrickey\endcsname{\Dartmouth@currentquestiondisplay}%
    \par\noindent
    % Question text with reserved space. Capture ordinary LaTeX labels while
    % preserving whatever \label implementation (e.g. hyperref) is active.
    \begin{minipage}[t]{\dimexpr\textwidth-4cm\relax}
        \textbf{\@currentquestionlabel} %
        \begingroup
            \let\Dartmouth@savedlabel\label
            \def\label##1{\Dartmouth@savedlabel{##1}\Dartmouth@recordquestionlabel{##1}}%
            \Dartmouth@insidequestiontrue
            #2%
            \Dartmouth@insidequestionfalse
        \endgroup
    \end{minipage}%
    \hfill
    % Point box positioned to align regardless of indentation
    \llap{\hspace{\@pointboxoffset}\begin{minipage}[t]{3.5cm}
        \raggedleft\small{[#3 pts]}
    \end{minipage}\hspace{0.5cm}}%
    \par\nopagebreak
}

\NewDocumentEnvironment{questionenv}{O{}}{%
    \Needspace{8\baselineskip}%
    % Don't process keys here anymore - they're processed in question commands

    \ifx\@questionFigure\empty
    \else
        \ifdefstring{\@questionFigureSide}{top}{%
            \centering\placefigure\par
            \vspace{1cm}%
        }{}%
        \ifdefstring{\@questionFigureSide}{left}{%
            \placefigure\hspace{5mm}%
            \begin{minipage}[t]{\dimexpr\linewidth-\@figureWidth-5mm\relax}%
        }{}%
        \ifdefstring{\@questionFigureSide}{right}{%
            \begin{minipage}[t]{\dimexpr\linewidth-\@figureWidth-5mm\relax}%
        }{}%
    \fi
}{%
    \ifx\@questionFigure\empty
    \else
        \ifdefstring{\@questionFigureSide}{left}{%
            \end{minipage}%
            \hspace{5mm}\placefigure
        }{}%
        \ifdefstring{\@questionFigureSide}{right}{%
            \end{minipage}%
            \hspace{5mm}\placefigure
        }{}%
        \ifdefstring{\@questionFigureSide}{bottom}{%
            \vspace{1cm}
            \centering\placefigure\par
        }{}%
    \fi
}

\NewDocumentCommand{\ShortAnswerQuestion}{O{} m m g}{%
    % Process keys first to get category information
    \resetquestionkeys
    \setkeys{question}{#1}%
    \@addquestionpoints{shortanswerpoints}{#3}
    \begin{questionblock}
        \begin{questionenv}[#1]
            {\typesetquestion{\thequestionnum}{#2}{#3}}
        \end{questionenv}
        \IfNoValueF{#4}{\Dartmouth@insidequestiontrue\DartmouthProcessInlineRubric{#4}\Dartmouth@insidequestionfalse}%
        \vspace{2mm}
        \noindent Answer: \underline{\hspace{\dimexpr\textwidth-7cm\relax}} % Space for the answer
        \vspace{5mm}
        \Dartmouth@rendercurrentrubric
    \end{questionblock}
}

\NewDocumentCommand{\MultipleChoiceQuestion}{O{} m m >{\SplitList{;}}m g}{%
    % Process keys first to get category information
    \resetquestionkeys
    \setkeys{question}{#1}%
    \@addquestionpoints{mcpoints}{#3}
    \begin{questionblock}
        \begin{questionenv}[#1]
            {\typesetquestion{\thequestionnum}{#2}{#3}}
        \end{questionenv}
        \begin{enumerate}[label=(\alph*), leftmargin=*, topsep=2pt, itemsep=1pt]
            \ProcessList{#4}{\insertchoice}
        \end{enumerate}
        \IfNoValueF{#5}{\Dartmouth@insidequestiontrue\DartmouthProcessInlineRubric{#5}\Dartmouth@insidequestionfalse}%
        \vspace{2mm}
        \noindent Answer: \underline{\hspace{\dimexpr\textwidth-7cm\relax}} % Space for the answer
        \vspace{5mm}
        \Dartmouth@rendercurrentrubric
    \end{questionblock}
}
\newcommand{\insertchoice}[1]{\item #1}

% True/False Question
\NewDocumentCommand{\TrueFalseQuestion}{O{} m m g}{%
    % Process keys first to get category information
    \resetquestionkeys
    \setkeys{question}{#1}%
    \@addquestionpoints{tfpoints}{#3}
    \begin{questionblock}
        \begin{questionenv}[#1]
            {\typesetquestion{\thequestionnum}{#2}{#3}}
        \end{questionenv}
        \IfNoValueF{#4}{\Dartmouth@insidequestiontrue\DartmouthProcessInlineRubric{#4}\Dartmouth@insidequestionfalse}%
        \vspace{2mm}
        \noindent Circle one: \quad \textbf{TRUE} \quad \textbf{FALSE}
        \vspace{5mm}
        \Dartmouth@rendercurrentrubric
    \end{questionblock}
}

% Fill in the Blank Question
\NewDocumentCommand{\FillInBlankQuestion}{O{} m m O{1} g}{%
    % Process keys first to get category information
    \resetquestionkeys
    \setkeys{question}{#1}%
    \@addquestionpoints{fillblankpoints}{#3}
    \begin{questionblock}
        \begin{questionenv}[#1]
            {\typesetquestion{\thequestionnum}{#2}{#3}}
        \end{questionenv}
        \vspace{2mm}
        \foreach \n in {1,...,#4}{%
            \noindent \underline{\hspace{\dimexpr\textwidth-5cm\relax}}\\[8pt]
        }
        \IfNoValueF{#5}{\Dartmouth@insidequestiontrue\DartmouthProcessInlineRubric{#5}\Dartmouth@insidequestionfalse}%
        \vspace{3mm}
        \Dartmouth@rendercurrentrubric
    \end{questionblock}
}

% Essay Question
\NewDocumentCommand{\EssayQuestion}{O{} m m O{10} g}{%
    \resetquestionkeys
    \setkeys{question}{#1}%
    \@addquestionpoints{essaypoints}{#3}
    \begin{questionblock}
        \begin{questionenv}[#1]
            {\typesetquestion{\thequestionnum}{#2}{#3}}
        \end{questionenv}
        \vspace{3mm}
        \foreach \n in {1,...,#4}{%
            \noindent \rule{\dimexpr\textwidth-4.5cm\relax}{0.4pt}\\[12pt]
        }
        \IfNoValueF{#5}{\Dartmouth@insidequestiontrue\DartmouthProcessInlineRubric{#5}\Dartmouth@insidequestionfalse}%
        \vspace{5mm}
        \Dartmouth@rendercurrentrubric
    \end{questionblock}
}

% Numerical Answer Question
\NewDocumentCommand{\NumericalQuestion}{O{} m m O{units} g}{%
    \resetquestionkeys
    \setkeys{question}{#1}%
    \@addquestionpoints{numericalpoints}{#3}
    \begin{questionblock}
        \begin{questionenv}[#1]
            {\typesetquestion{\thequestionnum}{#2}{#3}}
        \end{questionenv}
        \IfNoValueF{#5}{\Dartmouth@insidequestiontrue\DartmouthProcessInlineRubric{#5}\Dartmouth@insidequestionfalse}%
        \vspace{2mm}
        \noindent Answer: \underline{\hspace{\dimexpr\textwidth-7cm\relax}} #4
        \vspace{5mm}
        \Dartmouth@rendercurrentrubric
    \end{questionblock}
}

% Matching Question
\NewDocumentCommand{\MatchingQuestion}{O{} m m >{\SplitList{;}}m >{\SplitList{;}}m g}{%
    \resetquestionkeys
    \setkeys{question}{#1}%
    \@addquestionpoints{matchingpoints}{#3}
    \begin{questionblock}
        \begin{questionenv}[#1]
            {\typesetquestion{\thequestionnum}{#2}{#3}}
        \end{questionenv}
        \vspace{2mm}
        \noindent Match each item in Column A with the best answer in Column B.
        \vspace{2mm}
        
        \begin{minipage}[t]{0.45\textwidth}
            \textbf{Column A:}
            \begin{enumerate}[label=\arabic*., leftmargin=*, topsep=2pt, itemsep=2pt]
                \ProcessList{#4}{\insertmatchitem}
            \end{enumerate}
        \end{minipage}%
        \hfill
        \begin{minipage}[t]{0.45\textwidth}
            \textbf{Column B:}
            \begin{enumerate}[label=\alph*., leftmargin=*, topsep=2pt, itemsep=2pt]
                \ProcessList{#5}{\insertmatchitem}
            \end{enumerate}
        \end{minipage}
        
        \vspace{3mm}
        \noindent Answers: \@generateanswerblanks{#4}
        \IfNoValueF{#6}{\Dartmouth@insidequestiontrue\DartmouthProcessInlineRubric{#6}\Dartmouth@insidequestionfalse}%
        \vspace{5mm}
        \Dartmouth@rendercurrentrubric
    \end{questionblock}
}
\newcommand{\insertmatchitem}[1]{\item #1}

% Helper function to generate answer blanks based on number of items in Column A
\newcounter{matchitemcount}
\newcommand{\countmatchitem}[1]{\stepcounter{matchitemcount}}
\newcommand{\@generateanswerblanks}[1]{%
    \setcounter{matchitemcount}{0}%
    \ProcessList{#1}{\countmatchitem}%
    \foreach \n in {1,...,\thematchitemcount}{\n.\_\_\_ \quad}%
}

% Classification Question
\NewDocumentCommand{\ClassificationQuestion}{O{} m m >{\SplitList{;}}m >{\SplitList{;}}m g}{%
    \resetquestionkeys
    \setkeys{question}{#1}%
    \@addquestionpoints{classificationpoints}{#3}
    \begin{questionblock}
        \begin{questionenv}[#1]
            {\typesetquestion{\thequestionnum}{#2}{#3}}
        \end{questionenv}
        \vspace{2mm}
        
        % Display classification choices with highlighted first letters
        \noindent\textbf{Classification choices:} 
        \ProcessList{#5}{\insertclassificationchoice}
        \vspace{3mm}
        
        % Display items to classify
        \begin{enumerate}[label=\arabic*., leftmargin=*, topsep=2pt, itemsep=4pt]
            \ProcessList{#4}{\insertclassificationitem}
        \end{enumerate}
        \IfNoValueF{#6}{\Dartmouth@insidequestiontrue\DartmouthProcessInlineRubric{#6}\Dartmouth@insidequestionfalse}%
        \vspace{5mm}
        \Dartmouth@rendercurrentrubric
    \end{questionblock}
}
\newcommand{\insertclassificationchoice}[1]{\textbf{\underline{\MakeUppercase{\@firstletter{#1}}}}\@restofword{#1} \quad}
\newcommand{\insertclassificationitem}[1]{\item \underline{\hspace{2cm}} #1}

% Helper commands to extract first letter and rest of word
\def\@firstletter#1{\@firstletter@aux#1\relax}
\def\@firstletter@aux#1#2\relax{#1}
\def\@restofword#1{\@restofword@aux#1\relax}
\def\@restofword@aux#1#2\relax{#2}

% Invisible Question (for adding points to a category without rendering)
\NewDocumentCommand{\InvisibleQuestion}{O{} m}{%
    % #1 = key-value options, must include Category
    % #2 = points value
    
    % Process keys to find the category
    \resetquestionkeys
    \setkeys{question}{#1}%
    
    % Check if a category was actually provided
    \ifx\@questionCategory\empty
        % If no category, this command is meaningless. Issue an error.
        \PackageError{DartmouthExam}{InvisibleQuestion requires a 'Category' option}
            {You must specify a category, e.g., \string\InvisibleQuestion[Category=Bonus]{5}}%
    \else
        % Category was specified, so add points to it and the total.
        \addtocounter{totalpoints}{#2}%
        \expandafter\addtocounter\expandafter{\@questionCategory points}{#2}%
    \fi
    % --- End of command --- no visual output is generated.
}

\endinput

