Skip to content

Commit f96b40c

Browse files
authored
ezexam:0.1.7 (#3087)
1 parent 6c220ff commit f96b40c

File tree

13 files changed

+984
-0
lines changed

13 files changed

+984
-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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# `ezexam`
2+
## Introduction
3+
`This template is primarily designed to help Chinese primary, middle and high school teachers or students in creating exams or handouts.`
4+
5+
[Online Documentation](https://ezexam.pages.dev/)
6+
7+
8+
# Changelog
9+
10+
## 0 . 1 . 0
11+
12+
+ 初版发布
13+
14+
## 0 . 1 . 1
15+
16+
+ 修复 `choices` 方法中,若选项为图片时,设置宽度为百分比时,图片宽度无效的问题
17+
18+
## 0 . 1 . 2
19+
20+
+`secret` 修改为方法,可以自定义显示内容
21+
22+
+ 优化 `choices` 方法,当选项过长时,选项从第二行开始进行缩进。修复选项中既有文字又有图表时,标签和内容对不齐的问题
23+
24+
+`question` 方法的参数 `with-heading-label` 的默认值修改为 `false`
25+
26+
+ `explain` 方法新增参数 `show-number` 、修改参数 `title` 的默认值为 `none`,默认不显示
27+
28+
+ `setup` 方法新增参数 `enum-numbering`
29+
30+
## 0 . 1 . 3
31+
32+
+ 优化 `choices` 方法
33+
34+
+`question` 方法的参数名 `points-separate-par` 修改为 `points-separate`
35+
36+
+ 增加英文完型填空、7选5题型的支持,让 `paren``fillin` 方法可以使用题号作为占位符。使用详情查看 [`paren`](https://ezexam.pages.dev/paren)[`fillin`](https://ezexam.pages.dev/fillin) 方法
37+
38+
+ `setup` 方法新增参数 `heading-numbering``heading-hanging-indent``enum-spacing``enum-indent` 提供更多自定义设置
39+
40+
+ 修复 `question` 个数超过9个时,内容对不齐的问题
41+
42+
## 0 . 1 . 4
43+
44+
+`LECTURE` 修改为 `HANDOUTS`,更加符合语义
45+
46+
+`explain` 方法名修改为 `solution`,更加符合语义
47+
48+
+ 修复当修改弥封线类型时,试卷最后一页没有更改的 `bug`
49+
50+
+ 添加水印功能,`setup` 方法新增参数 `watermark``watermark-size``watermark-color``watermark-font``watermark-rotate`
51+
52+
## 0 . 1 . 5
53+
54+
+ 修复水印被图片遮挡的 `bug`
55+
56+
## 0 . 1 . 6
57+
58+
+ 修复有序列表,内容带有 `box` 时,编号和内容对不齐的 `bug`
59+
60+
+ 新增化学方程式的单线桥、双线桥的支持;原子、离子结构示意图的支持。使用详情查看 [`化学相关`](https://ezexam.pages.dev/chem)
61+
62+
## 0 . 1 . 7
63+
64+
+ 优化代码,确保 `heading-size` 只修改一级标题;并将其更名为 `h1-size`
65+
66+
+`title` 方法新增参数 `color`
67+
68+
+ 修复 `solution` 方法,当启用 `title` 时,如果解析内容过多,一页放不下,标题会跑到下一页的 `bug`;并将其参数 `above` 更名为 `top`;参数 `below` 更名为 `bottom`;统一参数名;添加参数 `padding-top``padding-bottom`
69+
70+
+ 去除 `question` 方法参数 `line-height`;该参数会影响题干之间的距离;该参数原本用于设置题目内容的行高,当题目中的公式比较高时,题号和题目内容会错位,这时可以通过该参数来微调。但是会造成内容每一行与行之间的间隔变大。可参考 [使用技巧](https://ezexam.pages.dev/tips) 代替;添加参数 `padding-top``padding-bottom`
71+
72+
+ 修复 `choices` 方法,调整其上下外边距导致选项之间的距离会跟着影响的 `bug`
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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.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+
准考证号: inline-square(14),
42+
考场号: inline-square(2),
43+
座位号: inline-square(2),
44+
),
45+
seal-line-type: "dashed",
46+
seal-line-supplement: "弥封线内不得答题",
47+
doc,
48+
) = {
49+
assert(mode in (HANDOUTS, EXAM), message: "mode must be HANDOUTS or EXAM")
50+
mode-state.update(mode)
51+
let _footer(label) = context {
52+
assert(
53+
type(label) in (str, function, none) or label == auto,
54+
message: "expected str or function or none or auto, found " + str(type(label)),
55+
)
56+
if label == none { return }
57+
let _label = label
58+
if label == auto {
59+
if mode == EXAM {
60+
_label = zh-arabic(suffix: "试题")
61+
} else {
62+
_label = "1 ✏ 1"
63+
}
64+
}
65+
// 如果传进来的label包含两个1,两个1中间不能是连续空格、包含数字
66+
// 支持双:阿拉伯数字、小写、大写罗马,带圈数字页码
67+
let reg-1 = "^[\D]*1[\D]*[^\d\s]+[\D]*1[\D]*$"
68+
let reg-i = reg-1.replace("1", "i")
69+
let reg-I = reg-1.replace("1", "I")
70+
let reg-circled-number = reg-1.replace("1", "")
71+
let reg-circled-number2 = reg-1.replace("1", "")
72+
let reg = reg-1 + "|" + reg-i + "|" + reg-I + "|" + reg-circled-number + "|" + reg-circled-number2
73+
74+
let current = counter(page).get()
75+
if (type(_label) == str and regex(reg) in _label) or (type(_label) == function) {
76+
current += counter(page).final()
77+
}
78+
79+
let _numbering = numbering(_label, ..current)
80+
81+
// 处于分栏下且左右页脚分离
82+
if page.columns == 2 and footer-is-separate {
83+
current.at(0) += 1
84+
grid(
85+
columns: (1fr, 1fr),
86+
align: center + horizon,
87+
// 左页码
88+
_numbering,
89+
// 右页码
90+
numbering(_label, ..current),
91+
)
92+
counter(page).step()
93+
return
94+
}
95+
96+
// 页面的页脚是未分离, 则让奇数页在右侧,偶数页在左侧
97+
let position = page-align
98+
if not footer-is-separate {
99+
if calc.odd(current.first()) {
100+
position = right
101+
} else {
102+
position = left
103+
}
104+
}
105+
align(position, _numbering)
106+
}
107+
let _header(
108+
student-info: seal-line-student-info,
109+
line-type: seal-line-type,
110+
supplement: seal-line-supplement,
111+
) = context {
112+
if mode != EXAM or not show-seal-line { return }
113+
// 根据页码决定是否显示弥封线
114+
// 如果当前页面有<title>,则显示弥封线,并在该章节最后一页的右侧也设置弥封线
115+
let chapter-location = for value in query(<title>) {
116+
counter(page).at(value.location())
117+
}
118+
119+
if chapter-location == none or chapter-location.len() == 0 { return }
120+
let current = counter(page).get().first()
121+
let last = counter(page).final()
122+
123+
// 获取上一章最后一页的页码,给最后一页加上弥封线
124+
let chapter-last-page-location = chapter-location.map(item => item - 1) + last
125+
if page.columns == 2 and footer-is-separate {
126+
chapter-last-page-location = chapter-location.map(item => item - 2) + (last.first() - 1,)
127+
}
128+
129+
// 去除第一章,因为第一章前面没有章节了
130+
let _ = chapter-last-page-location.remove(0)
131+
132+
let _margin-y = page.margin * 2
133+
let _width = page.height - _margin-y
134+
if page.flipped { _width = page.width - _margin-y }
135+
block(width: _width)[
136+
// 判断当前是在当前章节第一页还是章节最后一页
137+
//当前章节第一页弥封线
138+
#if chapter-location.contains(current) {
139+
place(
140+
dx: -_width - 1em,
141+
dy: -2em,
142+
)[
143+
#rotate(-90deg, origin: right + bottom)[
144+
#_create-seal(dash: line-type, info: student-info, supplement: supplement)
145+
]
146+
]
147+
return
148+
}
149+
150+
#if (chapter-last-page-location).contains(current) {
151+
_width = if page.flipped {
152+
page.height
153+
} else { page.width }
154+
// 章节最后页的弥封线
155+
place(dx: _width - page.margin - 2em, dy: 2em)[
156+
#rotate(90deg, origin: left + top, _create-seal(dash: line-type, supplement: supplement))
157+
]
158+
}
159+
]
160+
}
161+
let _background() = {
162+
if paper.columns == 2 and show-gap-line {
163+
line(angle: 90deg, length: 100% - paper.margin * 2, stroke: .5pt)
164+
}
165+
}
166+
let _foreground() = {
167+
if watermark == none { return }
168+
set text(size: watermark-size, watermark-color)
169+
set par(leading: .5em)
170+
place(horizon)[
171+
#grid(
172+
columns: paper.columns * (1fr,),
173+
..paper.columns * (rotate(watermark-rotate, watermark),),
174+
)
175+
]
176+
}
177+
set page(
178+
..paper,
179+
header: _header(),
180+
footer: _footer(page-numbering),
181+
background: _background(),
182+
foreground: _foreground(),
183+
)
184+
set columns(gutter: gap)
185+
186+
set outline(
187+
target: if mode == EXAM { <chapter> } else { heading },
188+
title: text(size: 15pt)[目#h(1em)录],
189+
)
190+
show outline: it => {
191+
set page(header: none, footer: _footer(outline-page-numbering))
192+
align(center, it)
193+
pagebreak(weak: true)
194+
counter(page).update(1) // 正文页码从1开始
195+
}
196+
197+
set par(leading: line-height, spacing: par-spacing, first-line-indent: (amount: first-line-indent, all: true))
198+
set text(font: font, size: font-size)
199+
200+
if heading-numbering == auto {
201+
if mode == EXAM {
202+
heading-numbering = "一、"
203+
heading-hanging-indent = 2.3em
204+
} else { heading-numbering = "1.1.1" }
205+
}
206+
set heading(numbering: heading-numbering, hanging-indent: heading-hanging-indent)
207+
show heading: it => {
208+
v(heading-top)
209+
text(heading-color, font: heading-font, it)
210+
v(heading-bottom)
211+
}
212+
213+
show heading.where(level: 1): it => {
214+
let size = h1-size
215+
if size == auto {
216+
if mode == HANDOUTS { size = text.size } else { size = 10.5pt }
217+
}
218+
text(size: size, it)
219+
}
220+
set enum(numbering: enum-numbering, spacing: enum-spacing, indent: enum-indent)
221+
set table(stroke: .5pt, align: center)
222+
set table.cell(align: horizon)
223+
224+
// 分段函数样式
225+
set math.cases(gap: 1em)
226+
// 显示方程编号
227+
set math.equation(numbering: "(1)", supplement: [Eq -]) if mode == HANDOUTS
228+
show math.equation: it => {
229+
// features: 一些特殊符号的设置,如空集符号设置更加漂亮
230+
set text(font: font-math, features: ("cv01",))
231+
// 1. 行内样式默认块级显示样式; 2. 添加数学符号和中文之间间距
232+
let space = h(.25em, weak: true)
233+
space + math.display(it) + space
234+
}
235+
236+
if show-answer {
237+
answer-state.update(true)
238+
answer-color-state.update(answer-color)
239+
}
240+
241+
doc
242+
}
243+

0 commit comments

Comments
 (0)