% \iffalse meta-comment % An Infrastructure for marking up Assignments % Copyright (c) 2019 Michael Kohlhase, all rights reserved % this file is released under the % LaTeX Project Public License (LPPL) % The original of this file is in the public repository at % http://github.com/sLaTeX/sTeX/ % \fi % % \iffalse % %<*driver> \def\stexdocpath{../doc} \input{\stexdocpath/stex-docheader} \stextoptitle{The \texttt{hwexam} Package}{hwexam} \docmodule{chapter} % % \fi % % \begin{stexmanual} % \begin{sfragment}{HWExam Manual} % \input{\stexdocpath/packages/stex-hwexam} % \end{sfragment} % \end{stexmanual} % % \begin{documentation} % \begin{sfragment}{HWExam Documentation} % TODO % \end{sfragment} % \end{documentation} % %\begin{implementation} % % \section{Implementation: The hwexam Package} % % \subsection{Package Options} % % The first step is to declare (a few) package options that handle whether certain % information is printed or not. Some come with their own conditionals that are set by the % options, the rest is just passed on to the |problems| package. % % \begin{macrocode} %<*package> \ProvidesExplPackage{hwexam}{2025/11/11}{4.0.0}{homework assignments and exams} \RequirePackage{l3keys2e} \keys_define:nn {hwexam / pkg}{ multiple .default:n = { false }, multiple .bool_set:N = \c_hwexam_multiple_bool, qrcode .default:n = { false }, qrcode .bool_set:N = \c_hwexam_qrcode_bool, unknown .code:n = { \PassOptionsToPackage{\CurrentOption}{problem} } } \ProcessKeysOptions{ hwexam /pkg } \RequirePackage{problem} % \end{macrocode} % % % \begin{macro}{\hwexam_kw_*} % For multilinguality, we define internal macros for keywords that can be specialized in % |*.ldf| files. % \begin{macrocode} \AddToHook{begindocument}{ \ExplSyntaxOn\makeatletter \input{hwexam-english.ldf} \ltx@ifpackageloaded{babel}{ \clist_set:Nx \l_tmpa_clist {\exp_args:No \tl_to_str:n \bbl@loaded} \exp_args:NNx \clist_if_in:NnT \l_tmpa_clist {\detokenize{ngerman}}{ \input{hwexam-ngerman.ldf} } \exp_args:NNx \clist_if_in:NnT \l_tmpa_clist {\detokenize{finnish}}{ \input{hwexam-finnish.ldf} } \exp_args:NNx \clist_if_in:NnT \l_tmpa_clist {\detokenize{french}}{ \input{hwexam-french.ldf} } \exp_args:NNx \clist_if_in:NnT \l_tmpa_clist {\detokenize{russian}}{ \input{hwexam-russian.ldf} } }{} \makeatother\ExplSyntaxOff } % \end{macrocode} % \end{macro} % % \subsection{QR Codes} % \begin{macrocode} \group_begin: \escapechar=-1 \xdef\_@@_qr_backslash{\string\\} \group_end: \bool_if:NT \c_hwexam_qrcode_bool { \RequirePackage{qrcode} \RequirePackage{marginnote} \str_new:N \g_@@_qr_json_str \bool_new:N \g_@@_qr_in_problems_json_bool \bool_set_false:N \g_@@_qr_in_problems_json_bool \bool_new:N \g_@@_qr_in_subproblems_json_bool \bool_set_false:N \g_@@_qr_in_subproblems_json_bool \bool_new:N \g_@@_qr_in_anscls_json_bool \bool_set_false:N \g_@@_qr_in_anscls_json_bool \bool_if:NTF \c__problems_gnotes_bool { \gdef \qrjson { \str_gput_right:Nx \g_@@_qr_json_str} }{ \gdef \qrjson #1 {} } \qrjson{[} \AtEndDocument{\qrjson{]}} \def\_@@_qr_escape_char:n #1 { #1\exp_args:Nno\use:nn{\if_charcode:w#1}\_@@_qr_backslash#1\fi } \gdef\_@@_qr_escape:n #1{\exp_args:Ne\str_map_function:nN{\tl_to_str:n{#1}}\_@@_qr_escape_char:n} \gdef \_@@_qr_escape:o #1{\exp_args:No\_@@_qr_escape:n#1} \def\qrschema{TO:DO:\examnumber} \bool_if:NTF \c__problems_test_bool { \def\doproblemqr{ \ifstexhtml\else{ \reversemarginpar\marginnote{\qrcode[height=1.5cm]{\qrschema}} \normalmarginpar }\fi } \def\insertexamnumber{ \ifstexhtml\else \tl_if_exist:NTF \examnumber { {\Large\bfseries ID:~\examnumber} }{ {\color{red}\Large\bfseries!!!~ WARNING:~NO~{\string\examnumber}~SET~!!!}\gdef\examnumber{0} } \global\def\insertexamnumber{} \fi } }{ \def\doproblemqr{} \def\insertexamnumber{} } \stexstyleproblem[noqr]{ \par\noindent\problemheader \xdef\qrid{\thesproblem} \bool_if:NT \c__problems_pts_bool { \tl_if_eq:NnF \l__problems_pts_tl {0}{ \marginpar{\l__problems_pts_tl{}~\problem@kw@points\smallskip} } } \bool_if:NT \c__problems_min_bool { \tl_if_eq:NnF \l__problems_min_tl {0} { \marginpar{\l__problems_min_tl{}~\problem@kw@minutes\smallskip} } } \par \stex_ignore_spaces_and_pars: }{ \par\bigskip } \stexstyleproblem{ \par\noindent\problemheader \xdef\qrid{\thesproblem} \doproblemqr \bool_if:NT \c__problems_pts_bool { \tl_if_eq:NnF \l__problems_pts_tl {0}{ \marginpar{\l__problems_pts_tl{}~\problem@kw@points\smallskip} } } \bool_if:NT \c__problems_min_bool { \tl_if_eq:NnF \l__problems_min_tl {0} { \marginpar{\l__problems_min_tl{}~\problem@kw@minutes\smallskip} } } \bool_if:NTF \g_@@_qr_in_problems_json_bool { \qrjson{,} }{ \bool_gset_true:N \g_@@_qr_in_problems_json_bool } \qrjson { \c_left_brace_str "id":"\_@@_qr_escape:o\l_stex_key_id_str","title":"\_@@_qr_escape:o\l_stex_key_title_tl", "number":"\thesproblem" } \tl_if_eq:NnF \l__problems_pts_tl {0}{ \qrjson { ,"pts":\l__problems_pts_tl } } \par \stex_ignore_spaces_and_pars: }{ \bool_if:NT \g_@@_qr_in_subproblems_json_bool { \qrjson {]} } \bool_gset_false:N \g_@@_qr_in_subproblems_json_bool \qrjson {\c_right_brace_str} \par\bigskip } \stexstylesubproblem[noqr]{ \begin{list}{}{ \setlength\topsep{0pt} \setlength\parsep{0pt} \setlength\rightmargin{0pt} }\item[\int_use:N \g__problems_subproblem_int .] \xdef\qrid{\thesproblem.\int_use:N \g__problems_subproblem_int} \bool_if:NT \c__problems_pts_bool { \bool_if:NF \l__problems_has_pts_bool { \marginpar{\smallskip\l_stex_key_pts_tl{}~\problem@kw@points} } } \bool_if:NT \c__problems_min_bool { \bool_if:NF \l__problems_has_min_bool{ \marginpar{\smallskip\l_stex_key_min_tl{}~\problem@kw@minutes} } } }{\end{list}} \stexstylesubproblem{ \begin{list}{}{ \setlength\topsep{0pt} \setlength\parsep{0pt} \setlength\rightmargin{0pt} }\item[\int_use:N \g__problems_subproblem_int .] \xdef\qrid{\thesproblem.\int_use:N \g__problems_subproblem_int} \doproblemqr \bool_if:NT \c__problems_pts_bool { \bool_if:NF \l__problems_has_pts_bool { \marginpar{\smallskip\l_stex_key_pts_tl{}~\problem@kw@points} } } \bool_if:NT \c__problems_min_bool { \bool_if:NF \l__problems_has_min_bool{ \marginpar{\smallskip\l_stex_key_min_tl{}~\problem@kw@minutes} } } \bool_if:NTF \g_@@_qr_in_subproblems_json_bool { \qrjson {,} }{ \qrjson { ,"subproblems":[ } \bool_gset_true:N \g_@@_qr_in_subproblems_json_bool } \qrjson { \c_left_brace_str "id":"\_@@_qr_escape:o\l_stex_key_id_str","title":"\_@@_qr_escape:o\l_stex_key_title_tl", "number":"\thesproblem.\int_use:N \g__problems_subproblem_int" } \bool_if:NF \l__problems_has_pts_bool { \qrjson { ,"pts":\l_stex_key_pts_tl } } }{ \qrjson {\c_right_brace_str} \end{list} } \stexstylegnote{ \par\smallskip\rule[.3em]{\linewidth}{0.4pt}\newline\smallskip \noindent\emph{\problem@kw@grading\str_if_empty:NF \l_stex_key_title_tl{ {~}\l_stex_key_title_tl } :~} \qrjson { ,"gnote": \c_left_brace_str "id":"\_@@_qr_escape:o\l_stex_key_id_str","title":"\_@@_qr_escape:o\l_stex_key_title_tl", "anscls":[ } }{ \qrjson { ]\c_right_brace_str } \par\rule[.3em]{\linewidth}{0.4pt}\newline } \renewcommand \anscls [2][] { \stex_keys_set:nn{ anscls }{#1} \str_if_empty:NT \l_stex_key_id_str { \int_incr:N \l__problems_anscls_int \str_set:Nx \l_stex_key_id_str { AC\int_use:N \l__problems_anscls_int } } \bool_if:NTF \g_@@_qr_in_anscls_json_bool { \qrjson{,} }{ \bool_set_true:N \g_@@_qr_in_anscls_json_bool } \qrjson{ \c_left_brace_str "id":"\_@@_qr_escape:o\l_stex_key_id_str","description":"\_@@_qr_escape:n{#2}", "feedback":"\_@@_qr_escape:o\l_stex_key_feedback_tl", "pts":"\l_stex_key_pts_str" \c_right_brace_str } \begin{list}{}{ \setlength\topsep{0pt} \setlength\parsep{0pt} \setlength\rightmargin{0pt} }\item[\l_stex_key_id_str] \stex_if_do_html:TF{ \exp_args:Ne \stex_annotate:nn{ data-ftml-answerclass={\l_stex_key_id_str} \str_if_empty:NF \l_stex_key_pts_str{ ,data-ftml-answerclass-pts={\l_stex_key_pts_str} } }{ #2 \tl_if_empty:NF \l_stex_key_feedback_tl{ \stex_annotate_invisible:nn{ data-ftml-answerclass-feedback={true} }{\l_stex_key_feedback_tl} } } }{#2} \str_if_empty:NF \l_stex_key_pts_str {\par ~ \problem@kw@points :~\l_stex_key_pts_str } \str_if_empty:NF \l_stex_key_feedback_tl {\par ~ \problem@kw@feedback :~\l_stex_key_feedback_tl } \end{list} } \stex_deactivate_macro:Nn \anscls {gnote~environments} \AtEndDocument{ \message{^^J^^J\g_@@_qr_json_str^^J^^J} \bool_if:NT \c__problems_gnotes_bool { \iow_new:N \c_@@_qr_json_iow \iow_open:Nn \c_@@_qr_json_iow {\jobname-vollkorn.json} \iow_now:Nx \c_@@_qr_json_iow {\g_@@_qr_json_str} \iow_close:N \c_@@_qr_json_iow } } \renewcommand\testemptypage[1][]{% \bool_if:NT \c__problems_test_bool {\ \xdef\qrid{P\thepage} \doproblemqr \vfill\begin{center}\hwexam@kw@testemptypage\end{center}\eject } } } % \end{macrocode} % % \subsection{Assignments} % % Then we set up a counter for problems and make the problem counter inherited from % |problem.sty| depend on it. Furthermore, we specialize the |\prob@label| macro to take % the assignment counter into account. % % \begin{environment}{assignment} % \begin{macrocode} \stex_keys_define:nnnn{ assignment }{ \tl_clear:N \l_stex_key_number_tl \tl_clear:N \l_stex_key_given_tl \tl_clear:N \l_stex_key_due_tl }{ number .tl_set:N = \l_stex_key_number_tl, given .tl_set:N = \l_stex_key_given_tl, due .tl_set:N = \l_stex_key_due_tl, unknown .code:n = {} }{id,title,style} \newcounter{assignment} \stex_new_stylable_env:nnnnnnn {assignment}{O{}}{ \cs_if_exist:NTF \l_hwexam_includeassignment_keys_tl { \tl_put_left:Nn \l_hwexam_includeassignment_keys_tl {#1,} \exp_args:Nno \stex_keys_set:nn{assignment}{ \l_hwexam_includeassignment_keys_tl } }{ \stex_keys_set:nn{assignment}{#1} } \tl_if_empty:NF \l_stex_key_number_tl { \global\setcounter{assignment}{\int_eval:n{\l_stex_key_number_tl-1}} } \global\refstepcounter{assignment} \setcounter{sproblem}{0} \def\thesproblem{\theassignment.\arabic{sproblem}} \stex_if_do_html:T{ \tl_if_empty:NF \l_stex_key_title_tl { \stexdoctitle \l_stex_key_title_tl } } \stex_style_apply: \_stex_do_id: }{ \stex_style_apply: }{ \par\begin{center} \textbf{\Large\assignmentautorefname~\theassignment \tl_if_empty:NF \l_stex_key_title_tl { {~}--~\l_stex_key_title_tl } }\par\smallskip \textbf{ \tl_if_empty:NF \l_stex_key_given_tl { \hwexam@kw@given :~\l_stex_key_given_tl\quad } \tl_if_empty:NF \l_stex_key_due_tl { \hwexam@kw@due :~\l_stex_key_due_tl\quad } } \end{center} \par\bigskip }{ \par\pagebreak }{} % \end{macrocode} % \end{environment} % % \begin{macro}{\includeassignment} % \begin{macrocode} \NewDocumentCommand\includeassignment{O{} m}{ \group_begin: \tl_set:Nn \l_hwexam_includeassignment_keys_tl {#1} \stex_keys_set:nn{includeproblem}{#1} \exp_args:Nno \use:nn{\inputref[}\l_stex_key_mhrepos_str]{#2} \group_end: } % \end{macrocode} % \end{macro} % % Restoring information about problems: % % \begin{macrocode} \prop_new:N \c_@@_problems_prop \tl_set:Nn \c_@@_total_mins_tl {0pt} \tl_set:Nn \c_@@_total_pts_tl {0pt} \int_new:N \c_@@_total_problems_int \cs_set_protected:Npn \problem@restore #1 #2 #3 { \int_gincr:N \c_@@_total_problems_int \prop_gput:Nnn \c_@@_problems_prop {#1}{{#2}{#3}} \tl_gset:Ne \c_@@_total_pts_tl { \dim_eval:n { \c_@@_total_pts_tl + #2pt }} \tl_gset:Ne \c_@@_total_mins_tl { \dim_eval:n { \c_@@_total_mins_tl + #2pt }} } % \end{macrocode} % % \begin{macro}{\correction@table} % This macro generates the correction table % \begin{macrocode} \newcommand\correction@table{ \int_compare:nNnT \c_@@_total_problems_int = 0 { \int_incr:N \c_@@_total_problems_int \prop_put:Nnn \c_@@_problems_prop {~}{{~}{~}} } \tl_clear:N \l_tmpa_tl \tl_clear:N \l_tmpb_tl \tl_clear:N \l_tmpc_tl \prop_map_inline:Nn \c_@@_problems_prop { \tl_put_right:Nn \l_tmpa_tl { ##1 & } \tl_put_right:Nx \l_tmpb_tl { \use_i:nn ##2 & } \tl_put_right:Nn \l_tmpc_tl { & } } \resizebox{\textwidth}{!}{% \exp_args:Nne \begin{tabular}{|l|*{\int_use:N \c_@@_total_problems_int}{c|}c||l|}\hline &\exp_args:Ne \multicolumn{\int_eval:n{ \c_@@_total_problems_int + 1}}{c||} {\footnotesize\hwexam@kw@forgrading} &\\\hline \hwexam@kw@probs & \l_tmpa_tl \hwexam@kw@sum & \hwexam@kw@grade\\\hline \hwexam@kw@pts & \l_tmpb_tl \dim_to_decimal:n\c_@@_total_pts_tl & \\\hline \hwexam@kw@reached & \l_tmpc_tl & \\[.7cm]\hline \end{tabular}}} % \end{macrocode} % \end{macro} % % \begin{macro}{\testheading} % \begin{macrocode} \def\hwexamheader{\input{hwexam-default.header}} \def\hwexamminutes{ \tl_if_empty:NTF \hwexam@duration { {\hwexam@min}~\hwexam@minutes@kw }{ \hwexam@duration } } \stex_keys_define:nnnn{ hwexam / testheading }{ \tl_clear:N \hwexam@min \tl_clear:N \hwexam@duration \tl_clear:N \hwexam@reqpts \tl_clear:N \hwexam@tools }{ min .tl_set:N = \hwexam@min, duration .tl_set:N = \hwexam@duration, reqpts .tl_set:N = \hwexam@reqpts, tools .tl_set:N = \hwexam@tools }{} \newenvironment{testheading}[1][]{ \stex_keys_set:nn { hwexam / testheading}{#1} \tl_set:Nx \hwexam@totalpts {\dim_to_decimal:n \c_@@_total_pts_tl} \tl_set:Nx \hwexam@totalmin {\dim_to_decimal:n \c_@@_total_mins_tl} \tl_set:Nx \hwexam@checktime {\dim_to_decimal:n { \hwexam@min pt - \hwexam@totalmin pt }} \newif\if@bonuspoints \tl_if_empty:NTF \hwexam@reqpts { \@bonuspointsfalse }{ \tl_set:Nx \hwexam@bonuspts { \dim_to_decimal:n{\hwexam@totalpts pt - \hwexam@reqpts pt} } \@bonuspointstrue } \makeatletter\hwexamheader\makeatother }{ \newpage } % \end{macrocode} % \end{macro} % % \begin{macro}{examdata,quizdata,homeworkdata} % % \begin{macrocode} \bool_new:N \g_@@_allow_data_bool \bool_gset_true:N \g_@@_allow_data_bool \AtBeginDocument{ \bool_gset_false:N \g_@@_allow_data_bool } \msg_set:nnn{stex}{hwexam/doubledata}{ Exam/Homework/Quiz~Data~already~set } \msg_set:nnn{stex}{hwexam/nodate}{ Exam/Homework/Quiz~Data~missing~date~value } \msg_set:nnn{stex}{hwexam/nocourse}{ Exam/Homework/Quiz~Data~missing~course~value } \stex_keys_define:nnnn{ hwexam / commondata }{ % https://docs.rs/dateparser/latest/dateparser/#accepted-date-formats \str_clear:N \l_@@_key_exam_date_str \str_clear:N \l_@@_key_exam_course_id_str \str_clear:N \l_@@_key_exam_term_str \int_set:Nn \l_@@_key_exam_num_int {1} }{ date .str_set:N = \l_@@_key_exam_date_str, course .str_set:N = \l_@@_key_exam_course_id_str, term .str_set:N = \l_@@_key_exam_term_str, num .int_set:N = \l_@@_key_exam_num_int }{} \stex_keys_define:nnnn{ hwexam / examdata }{ \bool_set_false:N \l_@@_key_exam_retake_bool }{ retake .bool_set:N = \l_@@_key_exam_retake_bool }{hwexam/commondata} \cs_set_protected:Nn \_@@_do_metadata:nnnn { \bool_if:NF \g_@@_allow_data_bool { \msg_fatal:nn{stex}{hwexam/doubledata} } \bool_gset_false:N \g_@@_allow_data_bool \stex_keys_set:nn { hwexam / #1}{#2} \str_set:Nn \g_stex_document_kind_str{#4} \str_if_empty:NT\l_@@_key_exam_date_str{ \msg_fatal:nn{stex}{hwexam/nodate} } \str_if_empty:NT\l_@@_key_exam_course_id_str{ \msg_fatal:nn{stex}{hwexam/nocourse} } \cs_if_exist:NT \date { \exp_args:No \date \l_@@_key_exam_date_str } \tl_set:Ne \g_stex_document_kind_parameters_tl{ ,data-ftml-document-kind-date={\l_@@_key_exam_date_str} ,data-ftml-document-kind-num=\int_use:N \l_@@_key_exam_num_int ,data-ftml-document-kind-course=\l_@@_key_exam_course_id_str \str_if_empty:NF \l_@@_key_exam_term_str{ ,data-ftml-document-kind-term=\l_@@_key_exam_term_str } #3 } } \newcommand\examdata[1]{ \_@@_do_metadata:nnnn{examdata}{#1}{ ,data-ftml-document-kind-retake=\bool_if:NTF \l_@@_key_exam_retake_bool{true}{false} }{exam} } \newcommand\homeworkdata[1]{ \_@@_do_metadata:nnnn{commondata}{#1}{}{homework} } \newcommand\quizdata[1]{ \_@@_do_metadata:nnnn{commondata}{#1}{}{quiz} } % \end{macrocode} % % \end{macro} % % % \begin{macrocode} % % \end{macrocode} % % \subsection{Leftovers} % % at some point, we may want to reactivate the logos font, then we use % \begin{verbatim} % here we define the logos that characterize the assignment % \font\bierfont=../assignments/bierglas % \font\denkerfont=../assignments/denker % \font\uhrfont=../assignments/uhr % \font\warnschildfont=../assignments/achtung % % \newcommand\bierglas{{\bierfont\char65}} % \newcommand\denker{{\denkerfont\char65}} % \newcommand\uhr{{\uhrfont\char65}} % \newcommand\warnschild{{\warnschildfont\char 65}} % \newcommand\hardA{\warnschild} % \newcommand\longA{\uhr} % \newcommand\thinkA{\denker} % \newcommand\discussA{\bierglas} % \end{verbatim} % \end{implementation} \endinput % \iffalse %%% Local Variables: %%% mode: doctex %%% TeX-master: t %%% End: % \fi % LocalWords: texttt scsys sc latexml fileversion filedate maketitle setcounter newpage % LocalWords: tocdepth tableofcontents pts showmeta showmeta showignores omdoc extrefs % LocalWords: testspace testnewpage testemptypage testheading testheading reqpts reqpts % LocalWords: exfig makeatletter makeatother vspace hrule vspace vspace noindent textsf % LocalWords: includeassignment includeassignment HorIacJuc cscpnrr11 importmodule baz % LocalWords: includemhassignment includemhassignment importmhmodule foobar ldots sref % LocalWords: mhcurrentrepos mh-variants mh-variant compactenum printbibliography Cwd % LocalWords: langle rangle langle rangle ltxml.cls ltxml.sty respetively metakeys qw % LocalWords: cwd stex graphicx amssymb amstext amsmath newif iftest testfalse testtrue % LocalWords: ifsolutions solutionsfalse ifmultiple multiplefalse multipletrue keyval % LocalWords: ltxml assig srefaddidkey addmetakey ifx assignment@titleblock stepcounter % LocalWords: document@hwexamtype importmodules metasetkeys inclassig@title inclassig % LocalWords: inclassig@title inclassig@type inclassig@type inclassig@number xspace kv % LocalWords: inclassig@number inclassig@due inclassig@due inclassig@given ignorespaces % LocalWords: inclassig@given newenvironment currentsectionlevel OptionalKeyVals kvi % LocalWords: omgroup vals hwexamtype ednote textbackslash newcommand inputassignment % LocalWords: unlist quizheading tas hspace hfill textbf newcount vfill addtocounter % LocalWords: theassignment@totalmin theassignment@totalpts assignment@probs xdef hline % LocalWords: assignment@totalpts assignment@totalmin correction@probs correction@probs % LocalWords: newcounter theassignment@probs footnotesize mh@currentrepos endinput % LocalWords: inclassig@mhrepos inclassig@mhrepos doctex inputmhassignment % LocalWords: GPL structuresharing STR iffalse cls NeedsTeXFormat hwexam hwexam.dtx sc