diff options
author | Karl Berry <karl@freefriends.org> | 2022-07-28 20:11:42 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2022-07-28 20:11:42 +0000 |
commit | b237c781052da2153e8e120f0e69909e54dad584 (patch) | |
tree | cb85963094b12969a2caeb82cc4688abc9028405 /Master/texmf-dist/tex/latex/exam-zh/exam-zh-choices.sty | |
parent | c617f3c41b0840c82ed1f943d62a60cdadb9b2e5 (diff) |
exam-zh (28jul22)
git-svn-id: svn://tug.org/texlive/trunk@63995 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/tex/latex/exam-zh/exam-zh-choices.sty')
-rw-r--r-- | Master/texmf-dist/tex/latex/exam-zh/exam-zh-choices.sty | 555 |
1 files changed, 555 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/latex/exam-zh/exam-zh-choices.sty b/Master/texmf-dist/tex/latex/exam-zh/exam-zh-choices.sty new file mode 100644 index 00000000000..6eccb24867a --- /dev/null +++ b/Master/texmf-dist/tex/latex/exam-zh/exam-zh-choices.sty @@ -0,0 +1,555 @@ +% +% Copyright (c) 2022 Zeping Lee +% Released under the LaTeX Project Public License v1.3c License. +% Repository: https://gitee.com/zepinglee/exam-zh +% + +\NeedsTeXFormat{LaTeX2e} + +\RequirePackage{expl3} +\RequirePackage{xparse} + +\ProvidesExplPackage {exam-zh-choices} {2022-07-28} {v0.1.13} + {exam-zh choices module} + +\dim_new:N \l__examzh_choices_column_sep_dim +\int_new:N \l__examzh_choices_columns_int +\tl_new:N \l__examzh_choices_label_tl +\tl_new:N \l__examzh_choices_label_pos_tl +\tl_new:N \l__examzh_choices_label_align_tl +\dim_new:N \l__examzh_choices_label_sep_dim +\dim_new:N \l__examzh_choices_label_width_dim +\int_new:N \l__examzh_choices_max_columns_int + +\keys_define:nn { exam-zh } + { choices .meta:nn = { exam-zh / choices } {#1} } + +\keys_define:nn { exam-zh / choices } + { + column-sep .dim_set:N = \l__examzh_choices_column_sep_dim , + columns .int_set:N = \l__examzh_choices_columns_int , + label .tl_set:N = \l__examzh_choices_label_tl , + label-pos .choices:nn = + { auto , top-left , left , bottom } + { \tl_set_eq:NN \l__examzh_choices_label_pos_tl \l_keys_choice_tl } , + label-align .tl_set:N = \l__examzh_choices_label_align_tl , + label-sep .dim_set:N = \l__examzh_choices_label_sep_dim , + label-width .dim_set:N = \l__examzh_choices_label_width_dim , + max-columns .int_set:N = \l__examzh_choices_max_columns_int , + index .int_set:N = \l__examzh_choices_item_index_int + } + +\keys_set:nn { exam-zh / choices } + { + column-sep = 1em , + columns = 0 , + label = \Alph*. , + label-pos = auto , + label-align = right , + label-sep = .5em , + label-width = 0pt , + max-columns = 4 , + index = 1, + } + +\NewDocumentCommand \setchoices { m } + { \keys_set:nn { exam-zh / choices } {#1} } + + +\tl_new:N \l__examzh_choices_counters_tl + +\NewDocumentCommand \AddChoicesCounter { m m } + % #1: \Alph(用户接口) + % #2: \@Alph(具体实现的命令或函数(开发层)) + { + % TODO 这一步的作用是什么,为什么要把函数放在 tl 变量里而不是直接在某处使用? + % 猜测:put_right 而不是 set,是为了保证操作 label 的输入值前 + % 几个函数都被 set + \tl_put_right:Nn \l__examzh_choices_counters_tl + { \__examzh_choices_process_counter:NN #1 #2 } + \cs_set_eq:cN { __examzh_choices_save_ \cs_to_str:N #1 : } #2 + \cs_set_eq:cN { __examzh_choices_save_ \cs_to_str:N #2 : } #2 + } + +\AddChoicesCounter \arabic \@arabic +\AddChoicesCounter \alph \@alph +\AddChoicesCounter \Alph \@Alph +\AddChoicesCounter \roman \@roman +\AddChoicesCounter \Roman \@Roman + + +\dim_new:N \l__examzh_choices_total_width_dim +\seq_new:N \l__examzh_choices_seq + +\NewDocumentEnvironment { choices } { O { } +b } + { + \par \nopagebreak + % 严格禁止孤行和寡行 + \int_set:Nn \clubpenalty { 10000 } + \int_set:Nn \widowpenalty { 10000 } + % 尽量避免在选项中间换行 + \int_set:Nn \interlinepenalty { 301 } + \noindent + \dim_set_eq:NN \l__examzh_choices_total_width_dim \linewidth + \int_zero:N \l__examzh_choices_columns_int + \dim_zero:N \l__examzh_choices_label_width_dim + \keys_set:nn { exam-zh / choices } {#1} + } + { + % 用 \item 分割选项 + \seq_set_split:Nnn \l__examzh_choices_seq { \item } {#2} + % 把第一个空项去掉 + \seq_if_empty:NF \l__examzh_choices_seq + { \seq_pop_left:NN \l__examzh_choices_seq \l_tmpa_tl } + % 收集正确的选项 + \__examzh_choices_collect_correct_choices:N + \l__examzh_choices_seq + % 计算标签和选项内容的最大自然宽度 + \__examzh_choices_calc_max_width:N \l__examzh_choices_seq + % label-pos = auto 时自动选择标签位置 + \__examzh_choices_set_auto_label_pos: + % 如果用户没有声明列数,计算合适的列数 + % 「学习点」从默认值是否被改变来测试用户是否输入 + % 用户没有输入的话就采用自动计算列数 + \int_compare:nNnT { \l__examzh_choices_columns_int } < {1} + { \__examzh_choices_calc_columns: } + % 计算每个选项内容的宽度 \l__examzh_choices_item_width_dim + \__examzh_choices_calc_item_width: + % 输出选项 + \__examzh_print_choices:N \l__examzh_choices_seq + % 输出正确选项 + \__examzh_print_correctchoice: + } + +% 用来存正确选项的序号(entry) +\seq_new:N \l__examzh_choices_correct_choices_label_seq +% 用来存正确选项的内容 +\seq_new:N \l__examzh_choices_correct_choices_item_seq + +% 收集正确的选项,保存在 \l__examzh_choices_correct_choices_seq +\cs_new:Npn \__examzh_choices_collect_correct_choices:N #1 + % #1: \l__examzh_choices_seq + { + \seq_clear:N \l__examzh_choices_correct_choices_label_seq + \seq_clear:N \l__examzh_choices_correct_choices_item_seq + \seq_clear:N \l_tmpa_seq + \seq_map_indexed_inline:Nn #1 + { + % ##1: 选项序号 + % ##2: 选项内容 + % 如果分割后,第一个字符是 * 的,表明这是一个正确选项 + %(即用 \item* 来标记正确答案) + \tl_if_head_eq_meaning:nNTF {##2} * + { + \seq_put_right:Nn \l__examzh_choices_correct_choices_label_seq + { \__examzh_choices_correct_choices_label_transfrom:n {##1} } + % 将去掉 * 号后的内容保存进 \l_tmpa_tl + \tl_set:Nx \l_tmpa_tl { \tl_tail:n {##2} } + % 去掉 * 和内容之间的空格 + \tl_trim_spaces:N \l_tmpa_tl + \seq_put_right:NV \l__examzh_choices_correct_choices_item_seq + \l_tmpa_tl + \seq_put_right:NV \l_tmpa_seq \l_tmpa_tl + } + { + \seq_put_right:Nn \l_tmpa_seq { ##2 } + } + } + \seq_set_eq:NN #1 \l_tmpa_seq + } + +% 根据 label 的样式 \l__examzh_choices_label_tl 转化正确选项的样式 +\cs_new:Npn \__examzh_choices_correct_choices_label_transfrom:n #1 + { + \group_begin: + \int_set:Nn \l__examzh_choices_index_int {#1} + % 定义计数器转换函数(如 \Alph 等) + \l__examzh_choices_counters_tl + % 输出 + \l__examzh_choices_label_tl + \group_end: + } +% 输出正确的选项 +\cs_new:Nn \__examzh_print_correctchoice: + { + \seq_if_empty:NF \l__examzh_choices_correct_choices_item_seq + { + \par + 参考答案: + \seq_use:Nn \l__examzh_choices_correct_choices_label_seq {,~} + } + } + + +\dim_new:N \l__examzh_choices_item_width_dim +\dim_new:N \l__examzh_choices_item_min_height_dim + +% 计算标签和选项内容的最大宽度, +% 分别保存到 \l__examzh_choices_label_width_dim 和 \l__examzh_choices_item_width_dim +% #1: \l__examzh_choices_seq +\cs_new:Npn \__examzh_choices_calc_max_width:N #1 + { + % 下面这两个的想法是 xchoices 项目可以优化学习的地方 + % 因为 xchoices 是把变量先设置为第一项的参数,然后让后面的和前面的比 + % 这里相当于把“设置为第一项的参数”这一步,用默认的“端点量”来代替 + % 比如取最大的,就和 0 比,这样的话其实也会产生变量会变成第一项的参数的结果 + % 但是两者性质不同,此处处理让 第一项 「没有特殊性」 + % 后面的计算最小高度的也是如此 + \dim_zero:N \l__examzh_choices_item_width_dim + \dim_set_eq:NN \l__examzh_choices_item_min_height_dim \c_max_dim + \seq_map_indexed_inline:Nn #1 + { + % -- 标签 -- + % 把标签整体放进 \l_tmpa_box + \hbox_set:Nn \l_tmpa_box { \__examzh_choices_the_label:n {##1} } + % 测量宽度 + \dim_set:Nn \l_tmpa_dim { \box_wd:N \l_tmpa_box } + % 与当前最大值比较,最后效果是 \l__examzh_choices_label_width_dim 储存了所有标签中宽度最大的标签的宽度值 + \dim_compare:nNnT + { \l_tmpa_dim } > { \l__examzh_choices_label_width_dim } + { \dim_set_eq:NN \l__examzh_choices_label_width_dim \l_tmpa_dim } + % -- 选项内容 -- + % 把内容放进 \l_tmpa_box 中 + \hbox_set:Nn \l_tmpa_box {##2} + % 测量宽度 + \dim_set:Nn \l_tmpa_dim { \box_wd:N \l_tmpa_box } + % 与当前最大值比较,最后效果是 \l__examzh_choices_item_width_dim 储存了所有选项内容中宽度最大的内容的宽度值 + \dim_compare:nNnT + { \l_tmpa_dim } > { \l__examzh_choices_item_width_dim } + { + \dim_set_eq:NN \l__examzh_choices_item_width_dim + \l_tmpa_dim + } + % -- 找到最小高度 -- + % 把内容高度储存到 \l_tmpb_dim + \dim_set:Nn \l_tmpb_dim { \box_ht:N \l_tmpa_box } + % 与当前最小值比较,最后效果是 \l__examzh_choices_item_min_height_dim 储存了所有内容中高度最小的内容的高度值 + \dim_compare:nNnT + { \l_tmpb_dim } < { \l__examzh_choices_item_min_height_dim } + { \dim_set_eq:NN \l__examzh_choices_item_min_height_dim \l_tmpb_dim } + \box_clear:N \l_tmpa_box + } + } + +% TODO 没看懂怎么实现的 +\int_new:N \l__examzh_choices_index_int + +% \Alph* 形式生成正确的标签 +\cs_new:Npn \__examzh_choices_the_label:n #1 + { + \group_begin: + \int_set:Nn \l__examzh_choices_index_int + { + \int_eval:n + { + \l__examzh_choices_item_index_int + #1 - 1 + } + } + \l__examzh_choices_counters_tl + \l__examzh_choices_label_tl + \group_end: + } + +\cs_new:Npn \__examzh_choices_process_counter:NN #1#2 + % #1: \Alph + % #2: \@Alph + { + % 用户可以同时使用 #1 和 #2 两个函数(命令)作为 label 的操作函数 + % #1 的内核原理函数是 #2 + \cs_set:Npn #1 { \__examzh_choices_process_counter_aux:Nn #2 } + \cs_set:Npn #2 { \__examzh_choices_process_counter_aux:Nn #2 } + } + +\cs_new:Npn \__examzh_choices_process_counter_aux:Nn #1#2 + % #1: \@Alph + { + \tl_if_eq:nnTF {#2} { * } + { + % 如果是 \alph* 类型的,效果为 \alph{ \l__examzh_choices_index_int } + \use:c { __examzh_choices_save_ \cs_to_str:N #1 : } + { \l__examzh_choices_index_int } + } + { + % 否则就是 \alph{...} 效果 + \use:c { __examzh_choices_save_ \cs_to_str:N #1 : } {#2} + } + } + + +% 超过这一高度阈值的选项视为插图模式 +% 注意使用 tl +% TODO 为何要使用 tl 而不用 dim ? +\tl_new:N \l__examzh_choices_figure_mode_threshold_tl +\tl_set:Nn \l__examzh_choices_figure_mode_threshold_tl { 2 \baselineskip } + +\cs_new:Npn \__examzh_choices_set_auto_label_pos: + { + \tl_if_eq:NnT \l__examzh_choices_label_pos_tl { auto } + { + % 若最小高度超过阈值,推测其中包含插图,将标签位置改为左居中 + \dim_compare:nNnTF + { \l__examzh_choices_item_min_height_dim } > + { \l__examzh_choices_figure_mode_threshold_tl } + { \tl_set:Nn \l__examzh_choices_label_pos_tl { left } } + { \tl_set:Nn \l__examzh_choices_label_pos_tl { top-left } } + } + } + + +\int_new:N \l__examzh_tmp_int + +% 计算选项的合适列数,存到 \l__examzh_choices_columns_int +\cs_new:Npn \__examzh_choices_calc_columns: + { + % 若标签不在底部,将 label-width 和 label-sep 加到 \l__examzh_choices_item_width_dim 里面 + \tl_if_eq:NnF \l__examzh_choices_label_pos_tl { bottom } + { + \dim_add:Nn \l__examzh_choices_item_width_dim + { \l__examzh_choices_label_width_dim + \l__examzh_choices_label_sep_dim } + } + % [总宽度 / 最大的选项宽度] = 列数 + % 这个计算出来是作为「算出来的、可以排的最大列数」 + % 将要和下面的手动(或者默认的)最大列数进行比较,来确定最后排多少列 + \int_set:Nn \l__examzh_choices_columns_int + { + \int_div_truncate:nn + { \l__examzh_choices_total_width_dim + \l__examzh_choices_column_sep_dim } + { \l__examzh_choices_item_width_dim + \l__examzh_choices_column_sep_dim } + } + % 如果上面的计算算出来是 0 的话,就设置为 1 + \int_compare:nNnTF { \l__examzh_choices_columns_int } = {0} + { \int_set:Nn \l__examzh_choices_columns_int {1} } + % 从允许的最大列数开始,每次除以 2,直到行宽允许排下 + % 比如设置了最大列数是 4 , 但是算出来可以排 5 + % 那么就会将 [4 / 2] = 2 < 5 作为列数 + \int_set_eq:NN \l__examzh_tmp_int \l__examzh_choices_max_columns_int + \int_while_do:nNnn + { \l__examzh_tmp_int } > { \l__examzh_choices_columns_int } + { + \int_set:Nn \l__examzh_tmp_int + { \int_div_truncate:nn { \l__examzh_tmp_int } {2} } + } + \int_set_eq:NN \l__examzh_choices_columns_int \l__examzh_tmp_int + } + + +% 计算选项的最终宽度,保存到 \l__examzh_choices_item_width_dim +\cs_new:Npn \__examzh_choices_calc_item_width: + { + \dim_set:Nn \l__examzh_choices_item_width_dim + { + % TODO 不是很理解这里的算法 + ( \l__examzh_choices_total_width_dim + - \l__examzh_choices_columns_int \l__examzh_choices_column_sep_dim + + \l__examzh_choices_column_sep_dim + ) / \l__examzh_choices_columns_int + } + % 若标签不在底部,将 label-width 和 label-sep 算进来 + % TODO 算进来?那怎么还是 sub? + \tl_if_eq:NnF \l__examzh_choices_label_pos_tl { bottom } + { + \dim_sub:Nn \l__examzh_choices_item_width_dim + { \l__examzh_choices_label_width_dim + \l__examzh_choices_label_sep_dim } + } + } + + +\int_new:N \l__examzh_choices_current_col_int + +% #1: \l__examzh_choices_seq +\cs_new:Npn \__examzh_print_choices:N #1 + { + \int_zero:N \l__examzh_choices_current_col_int + \seq_map_indexed_inline:Nn \l__examzh_choices_seq + { + \int_incr:N \l__examzh_choices_current_col_int + % 当前列号重置为 1 + \int_compare:nNnT + { \l__examzh_choices_current_col_int } > { \l__examzh_choices_columns_int } + { + % \par \noindent + \newline + \int_set:Nn \l__examzh_choices_current_col_int {1} + } + % TODO 为什么 > 1 才加呢? 这样的话第 1 列和第 2 列之间就没有这个间距? + \int_compare:nNnT { \l__examzh_choices_current_col_int } > {1} + { + \skip_horizontal:N \l__examzh_choices_column_sep_dim + % 增加一点弹性 + \skip_horizontal:n {0pt plus 1pt minus 1pt} + } + \__examzh_print_single_choice:nn {##1} {##2} + } + \par + } + + +\coffin_new:N \l__examzh_choices_item_coffin +\coffin_new:N \l__examzh_choices_label_coffin + +\cs_new:Npn \__examzh_print_single_choice:nn #1#2 + { + % 选项标签 + \__examzh_choices_make_label_coffin:n {#1} + % 选项内容 + \__examzh_choices_make_item_coffin:n {#2} + % 合并选项的标签和内容 + \str_case:Vn \l__examzh_choices_label_pos_tl + { + { top-left } + { + \coffin_join:NnnNnnnn + \l__examzh_choices_item_coffin {l} {H} + \l__examzh_choices_label_coffin {r} {H} + { - \l__examzh_choices_label_sep_dim } + { 0pt } + } + { left } + { + \coffin_join:NnnNnnnn + \l__examzh_choices_item_coffin {l} {vc} + \l__examzh_choices_label_coffin {r} {vc} + { - \l__examzh_choices_label_sep_dim } + { 0pt } + } + { bottom } + { + \coffin_join:NnnNnnnn + \l__examzh_choices_item_coffin {hc} {b} + \l__examzh_choices_label_coffin {hc} {t} + { 0pt } + % { - \l__examzh_choices_label_sep_dim } + { 0pt } + } + } + % 输出合并后 + \coffin_typeset:Nnnnn \l__examzh_choices_item_coffin {l} {H} {0pt} {0pt} + \coffin_clear:N \l__examzh_choices_item_coffin + \coffin_clear:N \l__examzh_choices_label_coffin + } + +% 将标签内容存入 coffin +\cs_new:Npn \__examzh_choices_make_label_coffin:n #1 + { + \hcoffin_set:Nn \l__examzh_choices_label_coffin + { + \hbox_to_wd:nn { \l__examzh_choices_label_width_dim } + { \__examzh_choices_make_label:n {#1} \strut } + } + } + +\cs_new:Npn \__examzh_choices_make_label:n #1 + { + \str_case:Vn \l__examzh_choices_label_align_tl + { + { left } { \rlap { \__examzh_choices_the_label:n {#1} } \hss } + { center } { \hss \clap { \__examzh_choices_the_label:n {#1} } \hss } + { right } { \hss \llap { \__examzh_choices_the_label:n {#1} } } + } + } + +\bool_new:N \l__examzh_choices_figure_mode_bool + +% 将选项内容存入 coffin +\cs_new:Npn \__examzh_choices_make_item_coffin:n #1 + { + \hcoffin_set:Nn \l__examzh_choices_item_coffin + { + % 优先尝试使用 hbox,这是因为在 \vcoffin_set 外部能保留原来的 \linewidth 和 + % \textwidth,方便用户在 \includegraphics 中使用 + \hbox_set:Nn \l_tmpa_box {#1} + % 若盒子的自然高度大于 2 行,且深度为 0pt,设置为插图模式 + \bool_lazy_and:nnT + { + \dim_compare_p:nNn { \box_ht:N \l_tmpa_box } > + { \l__examzh_choices_figure_mode_threshold_tl } + } + { \dim_compare_p:nNn { \box_dp:N \l_tmpa_box } < { 1pt } } + { \bool_set_true:N \l__examzh_choices_figure_mode_bool } + \vcoffin_set:Nnn \l_tmpa_coffin + { \l__examzh_choices_item_width_dim } + { + \dim_set_eq:NN \parskip \c_zero_dim + \dim_set_eq:NN \parindent \listparindent + \noindent + % \strut + % 若标签在底部,将图片居中对齐。 + \tl_if_eq:NnT \l__examzh_choices_label_pos_tl { bottom } + { \centering } + \dim_compare:nNnTF + { \box_wd:N \l_tmpa_box } > { \l__examzh_choices_item_width_dim } + { #1 } + { \box_use_drop:N \l_tmpa_box } + % 使用 \strut 将行距撑开,防止跟下一行选项的间距过小 + \mode_if_horizontal:T { \strut } + } + \dim_set:Nn \l_tmpa_dim { \coffin_ht:N \l_tmpa_coffin } + \bool_if:NT \l__examzh_choices_figure_mode_bool + { + \coffin_set_horizontal_pole:Nnn \l_tmpa_coffin {T} + { \l_tmpa_dim - 0.7 \baselineskip } + } + \coffin_typeset:Nnnnn \l_tmpa_coffin {l} {T} {0pt} {0pt} + \coffin_clear:N \l_tmpa_coffin + } + } + + +% 使用中文字体直接输出 unicode 带圈数字 +% \circlednumber 的参数既可以接受 LaTeX2e 的 <counter>,也可以直接接受 <intexpr>。 +% \NewDocumentCommand \circlednumber { m } +% { +% \int_if_exist:cTF { c@ #1 } +% { \int_set_eq:Nc \l_tmpa_int { c@#1 } } +% { \int_set:Nn \l_tmpa_int { #1 } } +% \exp_args:Nx \__examzh_choices_circled_number:n { \int_use:N \l_tmpa_int } +% } + +\cs_new:Npn \__examzh_choices_circled_number:n #1 + { + \int_set:Nn \l_tmpa_int {#1} + \int_compare:nNnTF { \l_tmpa_int } = { 0 } + { \int_set:Nn \l_tmpa_int { "24EA } } + { + \int_compare:nNnTF { \l_tmpa_int } < { 21 } + { \int_add:Nn \l_tmpa_int { "245F } } + { + \int_compare:nNnTF { \l_tmpa_int } < { 36 } + { \int_add:Nn \l_tmpa_int { "3250 } } + { + \int_compare:nNnTF { \l_tmpa_int } < { 51 } + { \int_add:Nn \l_tmpa_int { "32B0 } } + { + \msg_error:nnn { exam-zh / choices } + { invalid-circled-number } { \int_use:N \l_tmpa_int } + } + } + } + } + \group_begin: + % TODO 为何要用 \CJKfamily+ { } + % xeCJK 宏包文档:当 \CJKfamily+ 参数为空时,则使用当前的 CJK 字体族。 + \CJKfamily+ { } + \symbol { \l_tmpa_int } + \group_end: + } + +\msg_new:nnn { exam-zh / choices } { invalid-circled-number } + { Invalid~ circled~ number~ #1. } + +\AddChoicesCounter \circlednumber \__examzh_choices_circled_number:n + + +% TODO 答案控制 + +% 选择题答案控制 +% - 直接在后面显示 +% - 在括号内显示 +% - 手动输入 +% - 能否通过写中途文件方式使得答案可以出现在前面的括号内 +% - 统一移动到最后 +% - 也是 choices 的形式 +% - 表格形式 + +% 答案确定 +% 通过 \item 是否带 * 判断,有的话则标记为正确答案
\ No newline at end of file |