Skip to content

Commit ccdef69

Browse files
authored
ezexam:0.1.9 (#3123)
1 parent 4fce4c4 commit ccdef69

File tree

13 files changed

+1021
-0
lines changed

13 files changed

+1021
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 gbchu
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# `ezexam`
2+
![Typst Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fgbchu%2Fezexam%2Frefs%2Fheads%2Fmain%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)
3+
[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/gbchu/ezexam/blob/main/LICENSE)
4+
[![Online Documentation](https://img.shields.io/badge/online-Documentation-purple?logo=readthedocs)](https://ezexam.pages.dev/)
5+
6+
## Introduction
7+
This template is primarily designed to help Chinese primary, middle and high school teachers or students in creating exams or handouts.
8+
9+
## Changelog
10+
### 0 . 1 . 0
11+
+ 初版发布
12+
13+
### 0 . 1 . 1
14+
+ 修复 `choices` 方法中,若选项为图片时,设置宽度为百分比时,图片宽度无效的问题
15+
16+
### 0 . 1 . 2
17+
+`secret` 修改为方法,可以自定义显示内容
18+
+ 优化 `choices` 方法,当选项过长时,选项从第二行开始进行缩进。修复选项中既有文字又有图表时,标签和内容对不齐的问题
19+
+`question` 方法的参数 `with-heading-label` 的默认值修改为 `false`
20+
+ `explain` 方法新增参数 `show-number` 、修改参数 `title` 的默认值为 `none`,默认不显示
21+
+ `setup` 方法新增参数 `enum-numbering`
22+
23+
### 0 . 1 . 3
24+
+ 优化 `choices` 方法
25+
+`question` 方法的参数名 `points-separate-par` 修改为 `points-separate`
26+
+ 增加英文完型填空、7选5题型的支持,让 `paren``fillin` 方法可以使用题号作为占位符。使用详情查看 [`paren`](https://ezexam.pages.dev/reference/paren)[`fillin`](https://ezexam.pages.dev/reference/fillin) 方法
27+
+ `setup` 方法新增参数 `heading-numbering``heading-hanging-indent``enum-spacing``enum-indent` 提供更多自定义设置
28+
+ 修复 `question` 个数超过9个时,内容对不齐的问题
29+
30+
### 0 . 1 . 4
31+
+`LECTURE` 修改为 `HANDOUTS`,更加符合语义
32+
+`explain` 方法名修改为 `solution`,更加符合语义
33+
+ 修复当修改弥封线类型时,试卷最后一页没有更改的 `bug`
34+
+ 添加水印功能,`setup` 方法新增参数 `watermark``watermark-size``watermark-color``watermark-font``watermark-rotate`
35+
36+
### 0 . 1 . 5
37+
+ 修复水印被图片遮挡的 `bug`
38+
39+
### 0 . 1 . 6
40+
+ 修复有序列表,内容带有 `box` 时,编号和内容对不齐的 `bug`
41+
+ 新增化学方程式的单线桥、双线桥的支持;原子、离子结构示意图的支持。使用详情查看 [`化学相关`](https://ezexam.pages.dev/reference/chem)
42+
43+
### 0 . 1 . 7
44+
+ 优化代码,确保 `heading-size` 只修改一级标题;并将其更名为 `h1-size`
45+
+`title` 方法新增参数 `color`
46+
+ 修复 `solution` 方法,当启用 `title` 时,如果解析内容过多,一页放不下,标题会跑到下一页的 `bug`;并将其参数 `above` 更名为 `top`;参数 `below` 更名为 `bottom`;统一参数名;添加参数 `padding-top``padding-bottom`
47+
+ 去除 `question` 方法参数 `line-height`;该参数会影响题干之间的距离;该参数原本用于设置题目内容的行高,当题目中的公式比较高时,题号和题目内容会错位,这时可以通过该参数来微调。但是会造成内容每一行与行之间的间隔变大。可参考新增的参数 `padding-top``padding-bottom` 代替
48+
+ 修复 `choices` 方法,调整其上下外边距导致选项之间的距离会跟着影响的 `bug`
49+
50+
### 0 . 1 . 8
51+
+`mode` 添加新值 `SOLUTION`,当答案解析独立于试题存在时,使用此值可快速统一格式
52+
+ 优化 `choices` 方法;将其参数 `column` 更名为 `columns`,做到和官方的 `columns` 参数一致
53+
+ 废弃 `inline-square` 方法,推荐使用内置的 `table` 方法
54+
+ 修复 `color-box` 方法报错的 `bug`
55+
+ 优化 `secret``zh-arabic` 方法
56+
+ 优化 `question` 的编号实现方式;修改 `setup` 方法的参数 `enum-numbering` 的默认值为 `(1.i.a)`
57+
+ 优化 `notice` 方法;新增参数 `indent``hanging-indent`
58+
59+
### 0 . 1 . 9
60+
+ 优化 `text-figure` 方法;考虑到文本内容较多,为了书写方便,将参数 `text` 修改为位置参数;新增参数 `figure``style``gap`
61+
+ 优化 `question` 方法;修复当一个文档中组多套试卷时,会报警告的问题
62+
+ 优化 `title``score-box``scoring-box` 方法
63+
+ 添加 `cover` 方法
64+
+ 优化代码
65+
66+
67+
68+
69+
70+
71+
72+
73+
74+
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#import "lib/tools.typ": *
2+
#import "lib/outline.typ": *
3+
#import "lib/choice.typ": *
4+
#import "lib/question.typ": answer, fillin, fillinn, paren, parenn, question, score, solution, text-figure
5+
6+
#let setup(
7+
mode: HANDOUTS,
8+
paper: a4,
9+
page-numbering: auto,
10+
page-align: center,
11+
gap: 1in,
12+
show-gap-line: false,
13+
footer-is-separate: true,
14+
outline-page-numbering: "⚜ I ⚜",
15+
font-size: 11pt,
16+
font: source-han,
17+
font-math: source-han,
18+
line-height: 2em,
19+
par-spacing: 2em,
20+
first-line-indent: 0em,
21+
heading-numbering: auto,
22+
heading-hanging-indent: auto,
23+
h1-size: auto,
24+
heading-font: hei-ti,
25+
heading-color: luma(0%),
26+
heading-top: 10pt,
27+
heading-bottom: 15pt,
28+
enum-numbering: "(1.i.a)",
29+
enum-spacing: 2em,
30+
enum-indent: 0pt,
31+
watermark: none,
32+
watermark-color: rgb("#f666"),
33+
watermark-font: source-han,
34+
watermark-size: 88pt,
35+
watermark-rotate: -45deg,
36+
show-answer: false,
37+
answer-color: blue,
38+
show-seal-line: true,
39+
seal-line-student-info: (
40+
姓名: underline[~~~~~~~~~~~~~],
41+
准考证号: table(
42+
columns: 14,
43+
inset: .8em,
44+
[],
45+
),
46+
考场号: table(
47+
columns: 2,
48+
inset: .8em,
49+
[],
50+
),
51+
座位号: table(
52+
columns: 2,
53+
inset: .8em,
54+
[],
55+
),
56+
),
57+
seal-line-type: "dashed",
58+
seal-line-supplement: "弥封线内不得答题",
59+
doc,
60+
) = {
61+
assert(mode in (HANDOUTS, EXAM, SOLUTION), message: "mode must be HANDOUTS or EXAM or SOLUTION")
62+
mode-state.update(mode)
63+
let _footer(label) = context {
64+
assert(
65+
type(label) in (str, function, none) or label == auto,
66+
message: "expected str or function or none or auto, found " + str(type(label)),
67+
)
68+
if label == none { return }
69+
let _label = label
70+
if label == auto {
71+
if mode == HANDOUTS {
72+
_label = "1 ✏ 1"
73+
} else {
74+
let _prefix = [#subject-state.get()试题#if mode == SOLUTION [答案]]
75+
_label = zh-arabic(prefix: _prefix)
76+
}
77+
}
78+
// 如果传进来的label包含两个1,两个1中间不能是连续空格、包含数字
79+
// 支持双:阿拉伯数字、小写、大写罗马,带圈数字页码
80+
let reg-1 = "^[\D]*1[\D]*[^\d\s]+[\D]*1[\D]*$"
81+
let reg-i = reg-1.replace("1", "i")
82+
let reg-I = reg-1.replace("1", "I")
83+
let reg-circled-number = reg-1.replace("1", "")
84+
let reg-circled-number2 = reg-1.replace("1", "")
85+
let reg = reg-1 + "|" + reg-i + "|" + reg-I + "|" + reg-circled-number + "|" + reg-circled-number2
86+
87+
let current = counter(page).get()
88+
if (type(_label) == str and regex(reg) in _label) or (type(_label) == function) {
89+
current += counter(page).final()
90+
}
91+
92+
let _numbering = numbering(_label, ..current)
93+
94+
// 处于分栏下且左右页脚分离
95+
if page.columns == 2 and footer-is-separate {
96+
current.at(0) += 1
97+
grid(
98+
columns: (1fr, 1fr),
99+
align: center + horizon,
100+
// 左页码
101+
_numbering,
102+
// 右页码
103+
numbering(_label, ..current),
104+
)
105+
counter(page).step()
106+
return
107+
}
108+
109+
// 页面的页脚是未分离, 则让奇数页在右侧,偶数页在左侧
110+
let position = page-align
111+
if not footer-is-separate {
112+
if calc.odd(current.first()) {
113+
position = right
114+
} else {
115+
position = left
116+
}
117+
}
118+
align(position, _numbering)
119+
}
120+
let _header(
121+
student-info: seal-line-student-info,
122+
line-type: seal-line-type,
123+
supplement: seal-line-supplement,
124+
) = context {
125+
if mode != EXAM or not show-seal-line { return }
126+
// 根据页码决定是否显示弥封线
127+
// 如果当前页面有<title>,则显示弥封线,并在该章节最后一页的右侧也设置弥封线
128+
let chapter-location = for value in query(<title>) {
129+
counter(page).at(value.location())
130+
}
131+
132+
if chapter-location == none or chapter-location.len() == 0 { return }
133+
let current = counter(page).get().first()
134+
let last = counter(page).final()
135+
136+
// 获取上一章最后一页的页码,给最后一页加上弥封线
137+
let chapter-last-page-location = chapter-location.map(item => item - 1) + last
138+
if page.columns == 2 and footer-is-separate {
139+
chapter-last-page-location = chapter-location.map(item => item - 2) + (last.first() - 1,)
140+
}
141+
142+
// 去除第一章,因为第一章前面没有章节了
143+
let _ = chapter-last-page-location.remove(0)
144+
145+
let _margin-y = page.margin * 2
146+
let _width = page.height - _margin-y
147+
if page.flipped { _width = page.width - _margin-y }
148+
block(width: _width)[
149+
// 判断当前是在当前章节第一页还是章节最后一页
150+
//当前章节第一页弥封线
151+
#if chapter-location.contains(current) {
152+
place(
153+
dx: -_width - 1em,
154+
dy: -2.4em,
155+
)[
156+
#rotate(-90deg, origin: right + bottom)[
157+
#_create-seal(dash: line-type, info: student-info, supplement: supplement)
158+
]
159+
]
160+
return
161+
}
162+
163+
#if (chapter-last-page-location).contains(current) {
164+
_width = if page.flipped {
165+
page.height
166+
} else { page.width }
167+
// 章节最后页的弥封线
168+
place(dx: _width - page.margin - 2em, dy: 2em)[
169+
#rotate(90deg, origin: left + top, _create-seal(dash: line-type, supplement: supplement))
170+
]
171+
}
172+
]
173+
}
174+
let _background() = {
175+
if paper.columns == 2 and show-gap-line {
176+
line(angle: 90deg, length: 100% - paper.margin * 2, stroke: .5pt)
177+
}
178+
}
179+
let _foreground() = {
180+
if watermark == none { return }
181+
set text(size: watermark-size, watermark-color)
182+
set par(leading: .5em)
183+
place(horizon)[
184+
#grid(
185+
columns: paper.columns * (1fr,),
186+
..paper.columns * (rotate(watermark-rotate, watermark),),
187+
)
188+
]
189+
}
190+
set page(
191+
..a4 + paper,
192+
header: _header(),
193+
footer: _footer(page-numbering),
194+
background: _background(),
195+
foreground: _foreground(),
196+
)
197+
set columns(gutter: gap)
198+
199+
set outline(
200+
target: if mode == EXAM { <chapter> } else { heading },
201+
title: text(size: 15pt)[目#h(1em)录],
202+
)
203+
show outline: it => {
204+
set page(header: none, footer: _footer(outline-page-numbering))
205+
align(center, it)
206+
pagebreak(weak: true)
207+
counter(page).update(1) // 正文页码从1开始
208+
}
209+
210+
set par(leading: line-height, spacing: par-spacing, first-line-indent: (amount: first-line-indent, all: true))
211+
set text(font: font, size: font-size)
212+
213+
if heading-numbering == auto {
214+
if mode in (EXAM, SOLUTION) {
215+
heading-numbering = "一、"
216+
heading-hanging-indent = 2.3em
217+
} else { heading-numbering = "1.1.1" }
218+
}
219+
set heading(numbering: heading-numbering, hanging-indent: heading-hanging-indent)
220+
show heading: it => {
221+
v(heading-top)
222+
text(heading-color, font: heading-font, it)
223+
v(heading-bottom)
224+
}
225+
226+
show heading.where(level: 1): it => {
227+
let size = h1-size
228+
if size == auto {
229+
if mode == HANDOUTS { size = text.size } else { size = 10.5pt }
230+
}
231+
text(size: size, it)
232+
}
233+
set enum(numbering: enum-numbering, spacing: enum-spacing, indent: enum-indent)
234+
set table(stroke: .5pt, align: center)
235+
set table.cell(align: horizon)
236+
237+
// 分段函数样式
238+
set math.cases(gap: 1em)
239+
// 显示方程编号
240+
set math.equation(numbering: "(1)", supplement: [Eq -]) if mode == HANDOUTS
241+
show math.equation: it => {
242+
// features: 一些特殊符号的设置,如空集符号设置更加漂亮
243+
set text(font: font-math, features: ("cv01",))
244+
// 1. 行内样式默认块级显示样式; 2. 添加数学符号和中文之间间距
245+
let space = h(.25em, weak: true)
246+
space + math.display(it) + space
247+
}
248+
249+
if show-answer {
250+
answer-state.update(true)
251+
answer-color-state.update(answer-color)
252+
}
253+
254+
doc
255+
}
256+

0 commit comments

Comments
 (0)