diff --git a/packages/preview/ezexam/0.2.8/LICENSE b/packages/preview/ezexam/0.2.8/LICENSE
new file mode 100644
index 0000000000..4c2f9bb7bf
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 gbchu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/preview/ezexam/0.2.8/README.md b/packages/preview/ezexam/0.2.8/README.md
new file mode 100644
index 0000000000..619785a4e0
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/README.md
@@ -0,0 +1,165 @@
+# ezexam
+
+
+[](https://github.com/gbchu/ezexam/blob/main/LICENSE)
+[](https://ezexam.pages.dev/)
+
+
+This template is primarily designed to help Chinese university, primary, middle and high school teachers or students in creating exams or handouts.
+
+## Example
+```typst
+#import "@preview/ezexam:0.2.8": *
+#show: setup.with(
+ mode: EXAM,
+ paper: a3
+)
+
+#title[XX试卷]
+
+= 选择
+#question[
+ $(1 + 5i)i$ 的虚部为 #paren[]
+ #choices(-1, 0, 1, 6)
+]
+
+......
+
+= 填空
+#question[
+ 一个箱子里有 5 个球,分别以 1$~$5 标号,若有放回取三次,记至少取出一次的球的个数 $X$,则 $E(X) =$#fillin[].
+]
+
+......
+
+= 解答题
+#question(points: 15)[
+ 设数列 ${a_n}$ 满足 $a_1 = 3", "a_(n+1) / n = a_n / (n+1) + 1 / (n(n+1))$.
+ + 证明:${n a_n}$ 为等差数列;
+ + 设 $f(x) = a_1x + a_2x^2 + dots.c + a_m x^m,求 f'(-2)$.
+]
+
+......
+```
+
+## Changelog
+### 0 . 2 . 8
++ 更改 `question` 和 `solution` 方法的核心实现,彻底解决有较高的公式时,题号和题干对不齐的问题。删除这两个方法的 `padding-top` 和 `padding-bottom` 参数;`question` 方法新增参数 `hanging-indent` 并将参数 `body-indent` 修改为 `first-line-indent`
++ 修复以字符《 、【 、( 开头的题目或选项和标签间的距离过大的问题
++ 优化 `choice` 方法
++ 优化 `text-figure` 方法,删除 `align` 参数
++ 修复 `draft` 未引入 `title` 方法的引起的报错
++ `tag` 方法新增 `weight` 和 `x` 参数
++ 修复 `fillin` 方法有时不显示占位符的 bug
++ 将默认字体修改为 `roman` ,并添加 `TeX Gyre Termes` 字体,修复在线使用 `roman` 字体时,缺少 `Times New Roman` 字体造成的标题西文字体未正确设置的问题
++ 修改 `title` 方法的 `bottom` 参数和`subject` 方法的 `top` 参数的默认值为 0pt
++ `setup` 方法新增参数 `resume`
++ 废弃 `answer` 方法和 `multi` 常量
+
+### 0 . 2 . 7
++ 优化目录的显示效果
++ 优化 `fillin` 方法;修改其参数 `len` 的默认值为 1.5cm。修复当指定长度时,某些值会导致第一行线换行的 bug
++ 调整黑体的字体顺序
++ 修复以数学公式开头的选项,选项和 label 间会增加 .25em 的间距
+
+### 0 . 2 . 6
++ 修复 `fillin` 方法在页面分栏时,不能正确换行的 bug
+
+### 0 . 2 . 5
++ 废弃 `color-box` 方法 ;新增 `tag` 方法替代
++ 重构 `multi`
++ 将 `ROMAN` 修改为 `roman`
++ 优化字体,去掉 `noto serif sc` ;黑体新增 `Heiti SC` ,修复 Mac 用户本地使用时,黑体异常的 bug
++ 优化 `paren` 方法
++ 优化 `text-figure` 方法;修复当图文在新的一页最上面且文本较少时,图表显示不全的 bug ;修复当在一页的最后部分时,有较少部分文本留在当前页,图表也停留在当前页的 bug ,该 bug 使得图表遮挡前面的内容;添加参数 `align`
++ 优化 `fillin` 方法,修复多行线时,线之间的间隔不等的 bug
++ 废弃 `underdot` 方法;使用新的方法实现中文着重号
+
+### 0 . 2 . 4
++ 优化 `choices` 方法;新增参数 `label-position` ; 该参数可在选项为图表时,修改标签的位置。默认在左侧;将参数 `body-indent` 名修改为 `sapcing` ;更加符合语境
++ 优化 `question` 方法,修复题号偏移的 bug
++ 修复修改 `setup` 方法的 `paper` 参数时,只修改部分值报错的 bug
++ 修复在 Typst 的最新版 0.14.0 中数学字体报警告问题
+
+### 0 . 2 . 3
++ 为 `solution` 和 `question` 方法添加 `line-height` 参数;方便修改内容的行间距
++ 优化 `fillin` 方法
++ 修改 `question` 方法在 `HANDOUTS` 模式下的题号样式
++ 修复 `solution` 在 Typst 的最新版 0.14.0 中题号显示的 bug
++ 优化 `paren` 方法,默认填写英文不再大写
+
+### 0 . 2 . 2
++ 调整正文默认字体,由原来的 `Source Han Serif` 修改为 `Noto Serif SC` 、 `Noto Serif CJK SC` (二者效果一样,主要是后者压缩后更小,方便上传网盘进行安装)。黑体添加 `Noto Sans SC` 和 `Noto Sans CJK SC` ;修复在 typst app 中应用模板时,字体显示的问题
++ 添加常量 `ROMAN` ,方便修改字体为新罗马风格的字体,更加符合常见的试卷排版格式
++ 调整代码逻辑,使得在字体调整时,其它所有西文字体统一进行修改
++ 精简 `setup` 方法的参数,删除 `font-math` ;使用 `font` 参数即可完成正文字体和西文字体的设置
++ 调整 π 在罗马字体下显示的样式
++ 修改平行符号为倾斜
++ 修复 `choices` 某个选项有多行时,后续行的缩进和第一行不一致的bug
++ 重写 `fillin` 方法, 实现根据长度生成空线,并根据长度自动换行
++ 修复 `subject` 方法以字符串传入时的bug
++ 优化题号和题干之间的间距
+
+### 0 . 2 . 1
++ 修复试卷模式下,生成 pdf 后的书签会显示题目大标题的问题,确保只显示章节
++ 西文字体新增 Times New Roman 字体风格;前提是安装了 STIX 2 系列字体。如未安装则默认使用 New Computer Modern Math
++ 优化代码
+
+### 0 . 2 . 0
++ 添加 `cover` 方法;该方法可以生成一个封面
++ 添加 `underdot` 方法;在一些场景下,可以为文本添加着重号
+
+### 0 . 1 . 9
++ 优化 `text-figure` 方法;考虑到文本内容较多,为了书写方便,将参数 `text` 修改为位置参数;新增参数 `figure` 、`style` 、`gap`
++ 优化 `question` 方法;修复当一个文档中组多套试卷时,会报警告的问题
++ 优化 `title` 、`score-box` 、`scoring-box` 方法
++ 优化代码
+
+### 0 . 1 . 8
++ 为 `mode` 添加新值 `SOLUTION`,当答案解析独立于试题存在时,使用此值可快速统一格式
++ 优化 `choices` 方法;将其参数 `column` 更名为 `columns`,做到和官方的 `columns` 参数一致
++ 废弃 `inline-square` 方法,推荐使用内置的 `table` 方法
++ 修复 `color-box` 方法报错的 bug
++ 优化 `secret` 、`zh-arabic` 方法
++ 优化 `question` 的编号实现方式;修改 `setup` 方法的参数 `enum-numbering` 的默认值为 `(1.i.a)`
++ 优化 `notice` 方法;新增参数 `indent` 、`hanging-indent`
+
+### 0 . 1 . 7
++ 优化代码,确保 `heading-size` 只修改一级标题;并将其更名为 `h1-size`
++ 为 `title` 方法新增参数 `color`
++ 修复 `solution` 方法,当启用 `title` 时,如果解析内容过多,一页放不下,标题会跑到下一页的 bug;并将其参数 `above` 更名为 `top`;参数 `below` 更名为 `bottom`;统一参数名;添加参数 `padding-top`、`padding-bottom`
++ 去除 `question` 方法参数 `line-height`;该参数会影响题干之间的距离;该参数原本用于设置题目内容的行高,当题目中的公式比较高时,题号和题目内容会错位,这时可以通过该参数来微调。但是会造成内容每一行与行之间的间隔变大。可参考新增的参数 `padding-top`、`padding-bottom` 代替
++ 修复 `choices` 方法,调整其上下外边距导致选项之间的距离会跟着影响的 bug
+
+### 0 . 1 . 6
++ 修复有序列表,内容带有 `box` 时,编号和内容对不齐的 bug
++ 新增化学方程式的单线桥、双线桥的支持;原子、离子结构示意图的支持。使用详情查看 [`化学相关`](https://ezexam.pages.dev/reference/chem)
+
+### 0 . 1 . 5
++ 修复水印被图片遮挡的 bug
+
+### 0 . 1 . 4
++ 将 `LECTURE` 修改为 `HANDOUTS`,更加符合语义
++ 将 `explain` 方法名修改为 `solution`,更加符合语义
++ 修复当修改弥封线类型时,试卷最后一页没有更改的 bug
++ 添加水印功能,`setup` 方法新增参数 `watermark`,`watermark-size`,`watermark-color`,`watermark-font`,`watermark-rotate`
+
+### 0 . 1 . 3
++ 优化 `choices` 方法
++ 将 `question` 方法的参数名 `points-separate-par` 修改为 `points-separate`
++ 增加英文完型填空、7选5题型的支持,让 `paren` 和 `fillin` 方法可以使用题号作为占位符。使用详情查看 [`paren`](https://ezexam.pages.dev/reference/paren) 和 [`fillin`](https://ezexam.pages.dev/reference/fillin) 方法
++ `setup` 方法新增参数 `heading-numbering`,`heading-hanging-indent`,`enum-spacing`,`enum-indent` 提供更多自定义设置
++ 修复 `question` 个数超过9个时,内容对不齐的问题
+
+### 0 . 1 . 2
++ 将 `secret` 修改为方法,可以自定义显示内容
++ 优化 `choices` 方法,当选项过长时,选项从第二行开始进行缩进。修复选项中既有文字又有图表时,标签和内容对不齐的问题
++ 将 `question` 方法的参数 `with-heading-label` 的默认值修改为 `false`
++ `explain` 方法新增参数 `show-number` 、修改参数 `title` 的默认值为 `none`,默认不显示
++ `setup` 方法新增参数 `enum-numbering`
+
+### 0 . 1 . 1
++ 修复 `choices` 方法中,若选项为图片时,设置宽度为百分比时,图片宽度无效的问题
+
+### 0 . 1 . 0
++ 初版发布
diff --git a/packages/preview/ezexam/0.2.8/ezexam.typ b/packages/preview/ezexam/0.2.8/ezexam.typ
new file mode 100644
index 0000000000..8b8ed6a563
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/ezexam.typ
@@ -0,0 +1,278 @@
+#import "lib/tools.typ": draft, tag, zh-arabic
+#import "lib/outline.typ": *
+#import "lib/choice.typ": choices
+#import "lib/question.typ": question
+#import "lib/paren-fillin.typ": fillin, fillinn, paren, parenn
+#import "lib/solution.typ": *
+#import "lib/text-figure.typ": text-figure
+
+#let setup(
+ mode: HANDOUTS,
+ paper: a4,
+ page-numbering: auto,
+ page-align: center,
+ gap: 1in,
+ show-gap-line: false,
+ footer-is-separate: true,
+ outline-page-numbering: "I",
+ font: roman,
+ font-size: 11pt,
+ line-height: 2em,
+ par-spacing: 2em,
+ first-line-indent: 0em,
+ heading-numbering: auto,
+ heading-hanging-indent: auto,
+ h1-size: auto,
+ heading-font: heiti,
+ heading-color: luma(0),
+ heading-top: 10pt,
+ heading-bottom: 15pt,
+ enum-numbering: "(1.i.a)",
+ enum-spacing: 2em,
+ enum-indent: 0pt,
+ resume: true,
+ watermark: none,
+ watermark-color: rgb("#f666"),
+ watermark-font: roman,
+ watermark-size: 88pt,
+ watermark-rotate: -45deg,
+ show-answer: false,
+ answer-color: blue,
+ show-seal-line: true,
+ seal-line-student-info: (
+ 姓名: underline[~~~~~~~~~~~~~],
+ 准考证号: table(
+ columns: 14,
+ inset: .8em,
+ [],
+ ),
+ 考场号: table(
+ columns: 2,
+ inset: .8em,
+ [],
+ ),
+ 座位号: table(
+ columns: 2,
+ inset: .8em,
+ [],
+ ),
+ ),
+ seal-line-type: "dashed",
+ seal-line-supplement: "弥封线内不得答题",
+ doc,
+) = {
+ assert(mode in (HANDOUTS, EXAM, SOLUTION), message: "mode expected HANDOUTS, EXAM, SOLUTION")
+ assert(type(font) == array and type(heading-font) == array, message: "font must be an array")
+ mode-state.update(mode)
+ paper = a4 + paper
+ let _footer(label) = context {
+ assert(
+ type(label) in (str, function, none) or label == auto,
+ message: "page-numbering expected str, function, none, auto, found " + str(type(label)),
+ )
+ if label == none { return }
+ let _label = label
+ if label == auto {
+ _label = "1 / 1"
+ if mode != HANDOUTS {
+ _label = zh-arabic(prefix: [#subject-state.get()#if mode == SOLUTION [参考答案] else [试题]])
+ }
+ }
+ // 如果传进来的label包含两个1,两个1中间不能是连续空格、包含数字
+ // 支持双:阿拉伯数字、小写、大写罗马,带圈数字页码
+ let reg-1 = "^[\D]*1[\D]*[^\d\s]+[\D]*1[\D]*$"
+ let reg-i = reg-1.replace("1", "i")
+ let reg-I = reg-1.replace("1", "I")
+ let reg-circled-number = reg-1.replace("1", "①")
+ let reg-circled-number2 = reg-1.replace("1", "⓵")
+ let reg = reg-1 + "|" + reg-i + "|" + reg-I + "|" + reg-circled-number + "|" + reg-circled-number2
+
+ let current = counter(page).get()
+ if (type(_label) == str and regex(reg) in _label) or (type(_label) == function) {
+ current += counter(page).final()
+ }
+
+ let _numbering = numbering(_label, ..current)
+
+ // 处于分栏下且左右页脚分离
+ if page.columns == 2 and footer-is-separate {
+ current.at(0) += 1
+ grid(
+ columns: (1fr, 1fr),
+ align: center + horizon,
+ // 左页码
+ _numbering,
+ // 右页码
+ numbering(_label, ..current),
+ )
+ counter(page).step()
+ return
+ }
+
+ // 页面的页脚是未分离, 则让奇数页在右侧,偶数页在左侧
+ let position = page-align
+ if not footer-is-separate {
+ if calc.odd(current.first()) {
+ position = right
+ } else {
+ position = left
+ }
+ }
+ align(position, _numbering)
+ }
+ import "lib/tools.typ": _create-seal
+ let _header(
+ student-info: seal-line-student-info,
+ line-type: seal-line-type,
+ supplement: seal-line-supplement,
+ ) = context {
+ if mode != EXAM or not show-seal-line { return }
+ // 根据页码决定是否显示弥封线
+ // 如果当前页面有
,则显示弥封线,并在该章节最后一页的右侧也设置弥封线
+ let chapter-location = for value in query() {
+ counter(page).at(value.location())
+ }
+
+ if chapter-location == none or chapter-location.len() == 0 { return }
+ let current = counter(page).get().first()
+ let last = counter(page).final()
+
+ // 获取上一章最后一页的页码,给最后一页加上弥封线
+ let chapter-last-page-location = chapter-location.map(item => item - 1) + last
+ if page.columns == 2 and footer-is-separate {
+ chapter-last-page-location = chapter-location.map(item => item - 2) + (last.first() - 1,)
+ }
+ // 去除第一章,因为第一章前面没有章节了
+ let _ = chapter-last-page-location.remove(0)
+
+ let _margin-y = page.margin * 2
+ let _width = page.height - _margin-y
+ if page.flipped { _width = page.width - _margin-y }
+ block(width: _width)[
+ // 判断当前是在当前章节第一页还是章节最后一页
+ //当前章节第一页弥封线
+ #if chapter-location.contains(current) {
+ place(
+ dx: -_width - 1em,
+ dy: -2.4em,
+ rotate(-90deg, origin: right + bottom, _create-seal(
+ dash: line-type,
+ info: student-info,
+ supplement: supplement,
+ )),
+ )
+ return
+ }
+
+ // 章节最后页的弥封线
+ #if chapter-last-page-location.contains(current) {
+ _width = page.width
+ if page.flipped { _width = page.height }
+ place(
+ dx: _width - page.margin - 1em,
+ dy: 2em,
+ rotate(90deg, origin: left + top, _create-seal(
+ dash: line-type,
+ supplement: supplement,
+ )),
+ )
+ }
+ ]
+ }
+ let _background() = {
+ if paper.columns > 1 and show-gap-line {
+ line(angle: 90deg, length: 100% - paper.margin * 2, stroke: .5pt)
+ }
+ }
+ let _foreground() = {
+ if watermark == none { return }
+ set text(size: watermark-size, watermark-color)
+ set par(leading: .5em)
+ place(horizon, grid(
+ columns: paper.columns * (1fr,),
+ ..paper.columns * (rotate(watermark-rotate, watermark),),
+ ))
+ }
+ set page(
+ ..paper,
+ header: _header(),
+ footer: _footer(page-numbering),
+ background: _background(),
+ foreground: _foreground(),
+ )
+ set columns(gutter: gap)
+
+ set outline(
+ target: if mode == EXAM { } else { heading },
+ title: text(size: 15pt)[目#h(1em)录],
+ )
+ show outline: it => {
+ set page(header: none, footer: _footer(outline-page-numbering))
+ align(center, it)
+ pagebreak(weak: true)
+ counter(page).update(1) // 正文页码从1开始
+ }
+
+ set par(leading: line-height, spacing: par-spacing, first-line-indent: (amount: first-line-indent, all: true))
+ set text(font: font, size: font-size)
+
+ if heading-numbering == auto {
+ if mode in (EXAM, SOLUTION) {
+ heading-numbering = (..item) => numbering("一、", ..item) + h(-0.3em)
+ heading-hanging-indent = 2em
+ } else { heading-numbering = "1.1.1.1.1 " }
+ }
+ set heading(numbering: heading-numbering, hanging-indent: heading-hanging-indent)
+ show heading: it => {
+ v(heading-top)
+ text(heading-color, font: font.slice(0, -1) + heading-font, it)
+ v(heading-bottom)
+ if not resume { counter("question").update(0) }
+ }
+ show heading.where(level: 1): it => {
+ let size = h1-size
+ if size == auto {
+ if mode == HANDOUTS { size = 1em } else { size = 10.5pt }
+ }
+ text(size: size, it)
+ }
+ // 试卷模式下,书签只显示章节
+ set heading(bookmarked: false) if mode == EXAM
+ show heading.where(level: 1).and(): set heading(bookmarked: true)
+
+ set enum(numbering: enum-numbering, spacing: enum-spacing, indent: enum-indent)
+ set table.cell(align: horizon + center, stroke: .5pt)
+
+ // 分段函数样式
+ set math.cases(gap: 1em)
+ // 显示方程编号
+ set math.equation(numbering: "(1)", supplement: [Eq -]) if mode == HANDOUTS
+ show math.equation: it => {
+ // features: 一些特殊符号的设置,如空集符号设置更加漂亮
+ set text(font: font, features: ("cv01",))
+ // 1. 行内样式默认块级显示样式; 2. 添加数学符号和中文之间间距
+ let space = h(.25em, weak: true)
+ space + math.display(it) + space
+ }
+ // π 在类罗马字体 "TeX Gyre Termes Math" 下显示的样式;默认的有点丑
+ show math.pi: it => {
+ if "TeX Gyre Termes Math" in font {
+ return text(font: "Times New Roman", "π")
+ }
+ it
+ }
+ show math.parallel: "//"
+
+ // 中文着重号
+ show strong: content => {
+ show regex("\p{Hani}"): it => box(place(text("·", size: 0.8em), dx: 0.45em, dy: 0.75em) + it)
+ content.body
+ }
+
+ if show-answer {
+ answer-state.update(true)
+ answer-color-state.update(answer-color)
+ }
+
+ doc
+}
diff --git a/packages/preview/ezexam/0.2.8/lib/choice.typ b/packages/preview/ezexam/0.2.8/lib/choice.typ
new file mode 100644
index 0000000000..52a397aae6
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/lib/choice.typ
@@ -0,0 +1,91 @@
+#import "tools.typ": _content-start-space
+
+#let _format-choice(choice, label, indent, spacing, label-position) = {
+ // 为了解决数学公式在左侧加间距的问题
+ spacing -= _content-start-space(choice)
+ if choice.func() not in (image, table) {
+ return par(
+ hanging-indent: indent + spacing + measure(label).width,
+ h(indent) + label + h(spacing, weak: true) + choice,
+ )
+ }
+
+ // 选项为图片、表格的处理
+ if label-position == bottom {
+ return grid(
+ align: center,
+ inset: (left: indent),
+ pad(bottom: spacing, choice),
+ label,
+ )
+ }
+
+ grid(
+ columns: 2,
+ pad(left: indent, label), pad(left: spacing, choice),
+ )
+}
+
+#let _count-columns(container-width, choice-number, max-choice-width, columns) = {
+ // 如果未指定列数,则自动排列,默认4列
+ if columns == auto {
+ columns = 4
+ // 排成1行,选项之间的间距
+ let choice-gap = container-width / choice-number - max-choice-width
+ let min-gap = 0.15in
+ if choice-gap < min-gap {
+ columns = 2
+ // 排成2行,选项之间的间距
+ choice-gap = choice-gap * 2 + max-choice-width
+ if choice-gap < min-gap { columns = 1 }
+ }
+ }
+ columns
+}
+
+#let choices(
+ columns: auto,
+ c-gap: 0pt,
+ r-gap: 2em,
+ indent: 0pt,
+ spacing: 5pt,
+ top: 0pt,
+ bottom: 0pt,
+ label: "A.",
+ label-postion: left,
+ ..options,
+) = {
+ let args-named = options.named()
+ assert(args-named.len() == 0, message: "choices no " + repr(args-named) + " parameters")
+ // 使用layout获取当前父元素的宽度
+ layout(container => {
+ let choices-arr = options.pos()
+ let choice-number = choices-arr.len()
+ assert(choice-number > 0, message: "choices must have at least one option")
+ // 拼接选项并添加标签和间距;获取选项中最长的宽度
+ let max-width = 0pt
+ for index in range(choice-number) {
+ choices-arr.at(index) = _format-choice(
+ [#choices-arr.at(index)],
+ numbering(label, index + 1),
+ indent,
+ spacing,
+ label-postion,
+ )
+
+ if columns != auto { continue }
+ max-width = calc.max(max-width, measure(choices-arr.at(index)).width)
+ }
+
+ v(top)
+ grid(
+ columns: _count-columns(container.width, choice-number, max-width + c-gap, columns) * (1fr,),
+ column-gutter: c-gap,
+ row-gutter: r-gap,
+ align: horizon,
+ ..choices-arr
+ )
+ v(bottom)
+ })
+}
+
diff --git a/packages/preview/ezexam/0.2.8/lib/const-state.typ b/packages/preview/ezexam/0.2.8/lib/const-state.typ
new file mode 100644
index 0000000000..7afae85c57
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/lib/const-state.typ
@@ -0,0 +1,31 @@
+#let a3 = (
+ paper: "a3",
+ margin: 1in,
+ columns: 2,
+ flipped: true,
+)
+
+#let a4 = (
+ paper: "a4",
+ margin: 1in,
+ columns: 1,
+ flipped: false,
+)
+
+// #let main-font = ("New Computer Modern Math", "Noto Serif CJK SC")
+#let heiti = ("SimHei", "Heiti SC", "Noto Sans CJK SC")
+#let roman = (
+ (name: "Times New Roman", covers: regex("\w")),
+ (name: "TeX Gyre Termes", covers: regex("\w")),
+ "TeX Gyre Termes Math",
+ "Noto Serif CJK SC",
+)
+
+#let EXAM = "exam" // 试卷模式
+#let HANDOUTS = "handouts" // 讲义模式(默认)
+#let SOLUTION = "solution" // 解析模式
+
+#let mode-state = state("mode", HANDOUTS)
+#let answer-state = state("answer", false)
+#let answer-color-state = state("answer-color", blue)
+#let subject-state = state("subject", "")
diff --git a/packages/preview/ezexam/0.2.8/lib/outline.typ b/packages/preview/ezexam/0.2.8/lib/outline.typ
new file mode 100644
index 0000000000..3e61c03244
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/lib/outline.typ
@@ -0,0 +1,121 @@
+#import "const-state.typ": *
+
+// 封面
+#let cover(
+ title: "ezexam",
+ subtitle: none,
+ author: none,
+ date: auto,
+) = {
+ set page(footer: none, header: none, columns: 1)
+ set align(center + horizon)
+ text(size: 25pt)[#title]
+
+ if subtitle != none {
+ text(font: heiti, size: 22pt)[\ #subtitle]
+ }
+
+ if author != none {
+ text(font: "STKaiti", size: 15pt)[\ 作者:#author]
+ }
+
+ if date != none [
+ \ #if date == auto [
+ #datetime.today().year()/#datetime.today().month()/#(
+ datetime.today().day()
+ )]
+ ]
+}
+
+#let chapter(body) = {
+ pagebreak(weak: true)
+ counter("chapter").step()
+ set heading(numbering: _ => counter("chapter").display(it => box(width: .6em, align(right)[#it.~])))
+ place(hide[= #body ])
+}
+
+#let title(
+ body,
+ size: auto,
+ weight: 700,
+ font: auto,
+ color: luma(0),
+ position: center,
+ top: 0pt,
+ bottom: 0pt,
+) = context {
+ let _font = font
+ if _font == auto { _font = text.font }
+ let _size = size
+ if size == auto {
+ _size = 15pt
+ if mode-state.get() == HANDOUTS { _size = 20pt }
+ }
+ v(top)
+ align(position, text(font: _font, size: _size, weight: weight, color)[#body ])
+ v(bottom)
+ counter(heading).update(0)
+ counter("question").update(0)
+}
+
+#let subject(body, size: 21.5pt, spacing: 1em, font: heiti, top: 0pt, bottom: 0pt) = {
+ v(top)
+ align(center, text(
+ font: font,
+ size: size,
+ [#body].text.split("").slice(1, -1).join(h(spacing)),
+ ))
+ v(bottom)
+ subject-state.update([#body].text)
+}
+
+#let secret(body: [绝密★启用前]) = place(top, float: true, clearance: 20pt, text(font: heiti, body))
+
+#let exam-type(type, prefix: "试卷类型: ") = context place(top + right, text(
+ font: text.font.slice(0, -1) + heiti,
+)[#prefix#type])
+
+#let exam-info(
+ info: (
+ 时间: "120分钟",
+ 满分: "150分",
+ ),
+ weight: 500,
+ font: auto,
+ size: 1em,
+ gap: 2em,
+ top: 0pt,
+ bottom: 0pt,
+) = context {
+ assert(info.len() > 0, message: "info cannot be empty")
+ set text(font: text.font.slice(0, -1) + heiti, size: size, weight: weight)
+ set align(center)
+ grid(
+ columns: info.len(),
+ gutter: gap,
+ inset: (top: top, bottom: bottom),
+ align: center + horizon,
+ ..for (key, value) in info {
+ ([#key: #value],)
+ }
+ )
+}
+
+#let scoring-box(x: 0pt, y: 0pt) = place(dx: x, dy: y, right + top, table(
+ columns: 2,
+ inset: 8pt,
+)[得分][~~~~~~~~~][阅卷人])
+
+#let score-box(x: 0pt, y: 0pt) = place(dx: x, dy: y, right + top, table(
+ inset: 8pt,
+)[得分][~~~~~~~~~#v(10pt)])
+
+#let notice(label: "1.", indent: 2em, hanging-indent: auto, ..children) = context {
+ text(font: heiti)[注意事项:]
+ set enum(numbering: label, indent: indent)
+ set par(hanging-indent: if hanging-indent == auto {
+ -indent - enum.body-indent - measure(label).width
+ } else { hanging-indent })
+ for child in children.pos() [+ #par(child)]
+}
+
diff --git a/packages/preview/ezexam/0.2.8/lib/paren-fillin.typ b/packages/preview/ezexam/0.2.8/lib/paren-fillin.typ
new file mode 100644
index 0000000000..3b8d353a26
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/lib/paren-fillin.typ
@@ -0,0 +1,110 @@
+#import "const-state.typ": answer-color-state, answer-state
+
+#let _get-answer(body, placeholder, with-number, update) = {
+ if answer-state.get() {
+ return text(answer-color-state.get(), body)
+ }
+ if not with-number { return placeholder }
+ counter("placeholder").step()
+ context counter("placeholder").display()
+ if update { counter("question").step() }
+}
+
+#let _draw-line(len, stroke, offset, body) = {
+ let _len = len.to-absolute()
+ assert(_len > 4pt, message: "len must > 4pt")
+
+ set box(stroke: (bottom: stroke), inset: (bottom: offset), outset: (bottom: offset))
+
+ let page-width = page.width
+ if page.flipped { page-width = page.height }
+ let _columns = page.columns
+ let here-pos-x = here().position().x
+ if _columns > 1 {
+ let one-column-width = (page-width + columns.gutter * (_columns - 1)) / _columns
+ // 当有多个列时,当前内容所在的那一列加上前面所有的列的总宽度
+ page-width = one-column-width * calc.ceil(here-pos-x / one-column-width)
+ }
+
+ let first-line-available-space = page-width - page.margin - here-pos-x
+ let rest-len = _len - first-line-available-space
+ let is-line-break = false
+ let _space = 1pt
+ // 当前行剩余空间 < 10pt 时,则直接换行在新的一行从头开始画
+ if first-line-available-space < 10pt {
+ [ \ ]
+ is-line-break = true
+ rest-len = _len
+ } else {
+ // 当前指定长度 > 当前行剩余空间 >= 10pt,则按照当前行的剩余空间画线
+ // 如果当前指定长度 < 剩余空间,则按照指定长度在文字后画线
+ if rest-len < 0pt { first-line-available-space = _len }
+ // 第一行线
+ h(_space, weak: true)
+ box(width: first-line-available-space - _space, inset: 0pt, align(center, body))
+ box() // 解决第一行线换行问题
+ h(_space, weak: true)
+ }
+
+ // 超过一行的后续横线
+ if rest-len > 5pt {
+ // 计算可以画多少完整的条数
+ let _ratio = rest-len / (page.width - page.margin * 2)
+ // 多条完整线
+ // + "" 是为了解决多条线时,最后一行线与之前的线间距不等的问题
+ for _ in range(calc.trunc(_ratio)) {
+ (
+ box(width: 100%)[#if is-line-break {
+ align(center, body)
+ is-line-break = false
+ }]
+ + ""
+ )
+ }
+
+ // 最后一行的线
+ // + "" 是为了解决最后一行线,在这条线之后如果加文本线的间距变大问题
+ box(width: calc.fract(_ratio) * 100%)[#if is-line-break { align(center, body) }] + ""
+ h(_space, weak: true)
+ }
+}
+
+// 填空的横线
+#let fillin(
+ body,
+ len: 1.25cm,
+ placeholder: "\u{25B2}",
+ with-number: false,
+ update: false,
+ stroke: .45pt + luma(0),
+ offset: 3pt,
+) = context {
+ assert(type(len) == length, message: "expect length, got " + str(type(len)))
+ let result = _get-answer(body, placeholder, with-number, update)
+ if not answer-state.get() or result.child in ([], [ ]) {
+ return _draw-line(len, stroke, offset / 2, result)
+ }
+
+ underline(
+ evade: false,
+ offset: offset,
+ stroke: stroke,
+ result,
+ )
+}
+
+// 选项的括号
+#let paren(
+ body,
+ justify: false,
+ placeholder: "\u{25B2}",
+ with-number: false,
+ update: false,
+) = context [
+ #if justify { h(1fr) }
+ #h(0pt, weak: true)(~~#_get-answer(body, placeholder, with-number, update)~~)
+]
+
+// 类似英文中的7选5题型专用语法糖
+#let parenn = paren.with(with-number: true, update: true)
+#let fillinn = fillin.with(with-number: true, update: true)
diff --git a/packages/preview/ezexam/0.2.8/lib/question.typ b/packages/preview/ezexam/0.2.8/lib/question.typ
new file mode 100644
index 0000000000..dce893c87b
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/lib/question.typ
@@ -0,0 +1,84 @@
+#import "const-state.typ": HANDOUTS, mode-state
+#import "tools.typ": _content-start-space, _trim-content-start-parbreak
+
+#let _format-label(label, label-color, label-weight, with-heading-label) = context counter(
+ "question",
+).display(num => {
+ let _label = label
+ if label == auto {
+ _label = "1."
+ if mode-state.get() == HANDOUTS and with-heading-label {
+ _label = "1.1.1.1.1.1."
+ }
+ }
+ let arr = (num,)
+ if with-heading-label {
+ // 去除heading label数组中的0
+ arr = counter(heading).get().filter(item => item != 0) + arr
+ }
+
+ let result = text(
+ label-color,
+ weight: label-weight,
+ numbering(_label, ..arr),
+ )
+
+ if mode-state.get() == HANDOUTS { return result }
+ box(width: 1em, align(right, result))
+})
+
+#let _format-points(points, prefix, suffix, separate) = {
+ if points == none { return }
+ assert(type(points) == int and points > 0, message: "points be a positive integer!")
+ [#prefix#points#suffix#if separate [ \ ]]
+}
+
+#let question(
+ body,
+ indent: 0em,
+ first-line-indent: 0em,
+ hanging-indent: auto,
+ label: auto,
+ label-color: luma(0),
+ label-weight: 400,
+ with-heading-label: false,
+ points: none,
+ points-separate: true,
+ points-prefix: h(-0.45em, weak: true) + "(",
+ points-suffix: "分)",
+ line-height: auto,
+ top: 0pt,
+ bottom: 0pt,
+) = context {
+ counter("question").step()
+ set par(leading: line-height) if line-height != auto
+ let _label = _format-label(
+ label,
+ label-color,
+ label-weight,
+ with-heading-label,
+ )
+ let _hanging-indent = hanging-indent
+ if hanging-indent == auto { _hanging-indent = measure(_label).width + 1em }
+
+ v(top)
+ terms(
+ indent: indent,
+ hanging-indent: _hanging-indent,
+ separator: h(.9em, weak: true),
+ (
+ _label,
+ _format-points(
+ points,
+ points-prefix,
+ points-suffix,
+ points-separate,
+ )
+ + h(first-line-indent - _content-start-space[#body], weak: true)
+ + _trim-content-start-parbreak[#body],
+ ),
+ )
+ v(bottom)
+ // 更新占位符上的题号
+ context counter("placeholder").update(counter("question").get().first())
+}
diff --git a/packages/preview/ezexam/0.2.8/lib/solution.typ b/packages/preview/ezexam/0.2.8/lib/solution.typ
new file mode 100644
index 0000000000..9a3a6c6d64
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/lib/solution.typ
@@ -0,0 +1,79 @@
+#import "const-state.typ": answer-state
+#import "tools.typ": _trim-content-start-parbreak
+#let solution(
+ body,
+ title: none,
+ title-size: 12pt,
+ title-weight: 700,
+ title-color: luma(100%),
+ title-bg-color: maroon,
+ title-radius: 5pt,
+ title-align: top + center,
+ title-x: 0pt,
+ title-y: 0pt,
+ border-style: "dashed",
+ border-width: .5pt,
+ border-color: maroon,
+ color: blue,
+ radius: 5pt,
+ bg-color: luma(100%),
+ breakable: true,
+ line-height: auto,
+ top: 0pt,
+ bottom: 0pt,
+ inset: (x: 10pt, top: 20pt, bottom: 20pt),
+ show-number: true,
+) = context {
+ if not answer-state.get() { return }
+ assert(type(inset) == dictionary, message: "inset must be a dictionary")
+ v(top)
+ block(
+ width: 100%,
+ breakable: breakable,
+ inset: (top: 20pt, bottom: 20pt) + inset,
+ radius: radius,
+ stroke: (thickness: border-width, paint: border-color, dash: border-style),
+ fill: bg-color,
+ )[
+ // 标题
+ #if title != none {
+ let title-box = box(fill: title-bg-color, inset: 6pt, radius: title-radius, text(
+ size: title-size,
+ weight: title-weight,
+ tracking: 3pt,
+ title-color,
+ title,
+ ))
+ place(
+ title-align,
+ dx: title-x,
+ dy: -inset.top - measure(title-box).height / 2 + title-y,
+ title-box,
+ )
+ }
+
+ // 解析题号的格式化
+ #counter("explain").step()
+ #let _label = none
+ #if show-number {
+ _label = context numbering("1.", ..counter("explain").get())
+ }
+ #set par(leading: line-height) if line-height != auto
+ #let _space = 0em
+ #if show-number { _space = .75em }
+ #terms(
+ hanging-indent: 0em,
+ separator: h(_space, weak: true),
+ (
+ _label,
+ text(color, _trim-content-start-parbreak(body)),
+ ),
+ )
+ ]
+ v(bottom)
+}
+
+// 解析的分值
+#let score(points, color: maroon, score-prefix: h(.2em), score-suffix: "分") = text(color)[#box(width: 1fr, repeat(
+ $dot$,
+ ))#score-prefix#points#score-suffix]
diff --git a/packages/preview/ezexam/0.2.8/lib/text-figure.typ b/packages/preview/ezexam/0.2.8/lib/text-figure.typ
new file mode 100644
index 0000000000..5cd50328c0
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/lib/text-figure.typ
@@ -0,0 +1,35 @@
+// 图文混排(左文右图)
+#let text-figure(
+ figure: none,
+ figure-x: 0pt,
+ figure-y: 0pt,
+ top: 0pt,
+ bottom: 0pt,
+ gap: 0pt,
+ style: "tf",
+ text,
+) = context {
+ assert(style == "tf" or style == "ft", message: "style must be 'tf' or 'ft'")
+ let body = (
+ text, // [ \ ] 是为了在当前页还有一行时,换页
+ [ \ ] + box(place(dx: figure-x, dy: figure-y - par.leading * 2, figure)),
+ )
+
+ let _columns = (1fr, measure(figure).width)
+ let _gap = -figure-x + gap
+ if style == "ft" {
+ body = body.rev()
+ _columns = _columns.rev()
+ _gap = figure-x + gap
+ }
+
+ grid(
+ columns: _columns,
+ inset: (
+ top: top,
+ bottom: bottom,
+ ),
+ gutter: _gap,
+ ..body,
+ )
+}
diff --git a/packages/preview/ezexam/0.2.8/lib/tools.typ b/packages/preview/ezexam/0.2.8/lib/tools.typ
new file mode 100644
index 0000000000..9a378ea52e
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/lib/tools.typ
@@ -0,0 +1,90 @@
+#import "const-state.typ": heiti
+#import "outline.typ": title
+#let _special-char = "《(【"
+// 为了解决数学公式、特殊字符在最左侧没有内容时加间距的问题
+#let _math-or-special-char(body) = {
+ if body.func() == math.equation { return "math" }
+ if body.has("text") and body.text.first() in _special-char { "char" }
+}
+
+#let _check-content-starts-with(body) = {
+ if body.has("children") {
+ let children = body.children
+ if children.len() == 0 { return }
+ body = children.first()
+ if body == [ ] { body = children.at(1) }
+ }
+ _math-or-special-char(body)
+}
+
+#let _content-start-space(body) = {
+ if _check-content-starts-with(body) == "math" { return .25em }
+ if _check-content-starts-with(body) == "char" { return .4em }
+ 0em
+}
+
+#let _trim-content-start-parbreak(body) = {
+ if body.has("children") {
+ let children = body.children
+ if children.len() > 0 and children.first() == parbreak() {
+ return children.slice(1).join()
+ }
+ }
+ body
+}
+
+#let _create-seal(
+ dash: "dashed",
+ supplement: none,
+ info: (:),
+) = {
+ assert(type(info) == dictionary, message: "expected dictionary, found " + str(type(info)))
+ set par(spacing: 10pt)
+ set text(font: heiti, size: 12pt)
+ set align(center)
+ set grid(columns: 2, align: horizon, gutter: .5em)
+ if supplement != none { text(tracking: .8in, supplement) }
+ grid(
+ columns: if info.len() == 0 { 1 } else { info.len() },
+ gutter: 1em,
+ ..for (key, value) in info {
+ (
+ grid(
+ key,
+ value,
+ ),
+ )
+ }
+ )
+ line(length: 100%, stroke: (dash: dash))
+}
+
+#let draft(
+ name: "草稿纸",
+ student-info: (
+ 姓名: underline[~~~~~~~~~~~~~],
+ 准考证号: underline[~~~~~~~~~~~~~~~~~~~~~~~~~~],
+ 考场号: underline[~~~~~~~],
+ 座位号: underline[~~~~~~~],
+ ),
+ dash: "solid",
+ supplement: none,
+) = {
+ set page(margin: .5in, header: none, footer: none)
+ title(name.split("").join(h(1em)), bottom: 0pt)
+ _create-seal(dash: dash, supplement: supplement, info: student-info)
+}
+
+// 一种页码格式: "第x页(共xx页)
+#let zh-arabic(prefix: "", suffix: "") = (..nums) => {
+ let arr = nums.pos()
+ [#prefix 第#str(arr.at(0))页(共#str(arr.at(-1))页)#suffix]
+}
+
+#let tag(body, color: blue, font: auto, weight: 400, prefix: "【", suffix: "】", x: -.4em) = context {
+ let _font = font
+ if font == auto { _font = text.font.slice(0, -1) + heiti }
+ h(x, weak: true)
+ text(font: _font, weight: weight, color)[#prefix#body#suffix]
+ h(.1em, weak: true)
+}
diff --git a/packages/preview/ezexam/0.2.8/template/17.png b/packages/preview/ezexam/0.2.8/template/17.png
new file mode 100644
index 0000000000..c8482216f1
Binary files /dev/null and b/packages/preview/ezexam/0.2.8/template/17.png differ
diff --git a/packages/preview/ezexam/0.2.8/template/6.png b/packages/preview/ezexam/0.2.8/template/6.png
new file mode 100644
index 0000000000..b5cfd3c8f6
Binary files /dev/null and b/packages/preview/ezexam/0.2.8/template/6.png differ
diff --git a/packages/preview/ezexam/0.2.8/template/main.typ b/packages/preview/ezexam/0.2.8/template/main.typ
new file mode 100644
index 0000000000..876d2e6862
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/template/main.typ
@@ -0,0 +1,176 @@
+#import "@preview/ezexam:0.2.8": *
+
+#show: setup.with(mode: EXAM)
+
+#outline()
+#chapter[2025新高考I卷]
+#title[2025新高考I卷]
+#subject[数学]
+#secret()
+#scoring-box(y: .5in)
+#exam-type[A]
+#exam-info(info: (命题人: "张三 李四 王五", 审题: "老六教研组"))
+#exam-info()
+
+#notice(
+ [答题前,请务必将自已的姓名、准考证号用0.5毫米黑色墨水的签字笔填写在试卷及答题卡的规定位置。],
+ [请认真核对监考员在答题卡上所粘贴的条形码上的姓名、准考证号与本人是否相符。],
+ [作答选择题必须用2B铅笔将答题卡上对应选项的方框涂满、涂黑;如需改动,请用橡皮擦干净后,再选涂其他答案。作答非选择题,必须用0.5毫米黑色墨水的签字笔在答题卡上的指定位置作答,在其他位置作答一律无效。],
+ [本试卷共4页,满分150分,考试时间为120分钟。考试结束后,请将本试卷和答题卡一并交回。],
+)
+
+= 单选题:本题共 8 小题,每小题 5 分,共 40 分。在每小题给出的四个选项中,只有一项是符合题目要求的。
+#question[
+ $(1 + 5i)i$ 的虚部为 #paren[C]
+ #choices(-1, 0, 1, 6)
+]
+
+#question[
+ 集合 $U = {x | x$为小于9的正整数}, $A = {1,3,5}$, 则 $complement_U A$ 中的元素个数为 #paren[C]
+ #choices(0, 3, 5, 8)
+]
+
+#question[
+ 若双曲线 $C$ 的虚轴长为实轴长的 $sqrt(7)$ 倍,则 $C$ 的离心率为 #paren[D]
+ #choices([$sqrt(2)$], [$2$], [$sqrt(7)$], [$2sqrt(2)$])
+]
+
+#question[
+ 若点 $(a,0) (a > 0)$ 是函数 $y = 2tan(x - pi / 3)$ 的图象的一个对称中心,则 $a$ 的最小值为 #paren[B]
+ #choices([30°], [60°], [90°], [135°])
+]
+
+#question[
+ 设 $f(x)$ 是定义在 $RR$ 上且周期为 2 的偶函数,当 $2 lt.slant x lt.slant 3$ 时,$f(x) = 5 - 2x$,则 $f(-3 / 4 ) =$
+ #paren[A]
+ #choices([$-1 / 2$], [$-1 / 4$], [$1 / 4$], [$1 / 2$])
+]
+
+#question[
+ 已知视风速是真风速和船风速的和向量,船风速与船行驶速度大小相等,方向相反.则真风速等级是 #paren[A]
+ #text-figure(
+ figure: image("6.png", height: 1.5in),
+ figure-x: -1in,
+ )[#choices(
+ columns: 1,
+ [轻风 (1.6$~$3.3 m/s)],
+ [微风 (3.4$~$5.4 m/s)],
+ [和风 (5.5$~$7.8 m/s)],
+ [劲风 (8.0$~$10.7 m/s)],
+ )]
+]
+
+#question[
+ 若圆 $x^2 + (y + 2)^2 = r^2 (r > 0)$ 上到直线 $y = sqrt(3)x + 2$ 的距离为 1 的点有且仅有 2 个,则 $r$ 的取值范围是
+ #paren[B]
+ #choices([(0, 1)], [(1, 3)], [(3, +∞)], [(0, +∞)])
+]
+
+#question[
+ 若实数 $x, y, z$ 满足 $2 + log_2 x = 3 + log_3y = 5 + log_5 z$,则 $x, y, z$ 的大小关系不可能是 #paren[B]
+ #choices([$x > y > z$], [$x > z > y$], [$y > x > z$], [$y > z > x$])
+]
+
+= 多选题:本题共 3 小题,每小题 6 分,共 18 分.在每小题给出的选项中,有多项符合题目要求。全部选对的得 6 分,部分选对的得部分分,有选错的得 0 分。
+#question[
+ 在正三棱柱 $A B C-A_1B_1C_1$ 中,$D$ 为 $B C$ 中点,则 #paren[BD]
+ #choices([$A D perp A_1C$], [$B_1C perp "平面" A A_1D$], [$C C_1 parallel "平面" A A_1D$], [$A D parallel A_1B_1$])
+]
+
+#question[
+ 设抛物线 $C: y^2 = 6x$ 的焦点为 $F$,过 $F$的直线交 $C$ 于$A、B$,过 $F$ 且垂直于 $A B$的直线交准线 $l$: $y = -3 / 2x$
+ 于 $E$,过点$A$作准线的垂线,垂足为$D$,则 #paren[ACD]
+ #choices([$|A D| = |A F|$], [$|A E| = |A B|$], [$|A B| gt.slant 6$], [$|A E| dot |B E| gt.slant 18$])
+]
+
+#question[
+ 已知 $triangle A B C$ 的面积为 $1 / 4$,若 $cos 2A + cos 2B + cos 2C = 2,cos A cos B sin C = 1 / 4$,则 #paren[ACD]
+ #choices([$sin C = sin^2 A + sin^2 B$], [$A B = sqrt(2)$], [$sin A + sin B = sqrt(6) / 2$], [$A C^2 + B C^2 = 3$])
+]
+
+= 填空题:本题共 3 小题,每小题 5 分,共 15 分。
+#question[
+ 若直线 $y = 2x +5$ 是曲线 $y = e^x + x + a$ 的切线,则 $a =$#fillin[4].
+]
+
+#question[
+ 若一个正项等比数列的前 4 项和为 4,前 8 项和为 68,则该等比数列的公比为 #fillin[$plus.minus 2$].
+]
+
+#question[
+ 一个箱子里有 5 个球,分别以 1$~$5 标号,若有放回取三次,记至少取出一次的球的个数 $X$,则 $E(X) =$#fillin[$61/25$].
+]
+
+= 解答题:本题共 5 小题,共 77 分.解答应写出文字说明、证明过程或演算步骤。
+#question(points: 13, bottom: 2in)[
+ 为研究某疾病与超声波检查结果的关系,从做过超声波检查的人群中随机调查了1000人,得到如下的列联表:
+ #align(center)[
+ #table(
+ columns: 4,
+ [], [正常], [不正常], [合计],
+ [患该疾病], [20], [180], [200],
+ [未患该疾病], [780], [20], [800],
+ [合计], [800], [200], [1000],
+ )
+ ]
+ + 记超声波检查结果不正常者患有该疾病的概率为$p$,求$p$的估计值;
+ + 根据小概率值$alpha=0.001$的独立性检验,分析超声波检查结果是否与患该疾病有关.
+
+ #text-figure(
+ figure: table(
+ columns: 4,
+ [$P(chi^2 gt.slant k)$], [0.005], [0.010], [0.001],
+ [$k$], [3.841], [6.635], [10.828],
+ ),
+ )[附:$chi^2 = n(a d - b c)^2 / ((a + b)(c + d)(a + c)(b + d))$.]
+]
+
+#question(points: 15, bottom: 1in)[
+ 设数列 ${a_n}$ 满足 $a_1 = 3", "a_(n+1) / n = a_n / (n+1) + 1 / (n(n+1))$.
+ + 证明:${n a_n}$ 为等差数列;
+ + 设 $f(x) = a_1x + a_2x^2 + dots.c + a_m x^m,求 f'(-2)$.
+]
+
+#question(points: 15, bottom: 2in)[
+ 如图所示的四棱锥 $P - A B C D$ 中,$P A perp "平面" A B C D, B C parallel A D, A B perp A D$.
+ + 证明:平面 $P A B perp "平面" P A D$
+ + 若 $P A = A B = sqrt(2), A D = sqrt(3) + 1, B C = 2$,$P, B, C, D$ 在同一个球面上,设该球面的球心为 $O$.
+ #text-figure(
+ figure: image("17.png", height: 1.6in),
+ figure-x: 20pt,
+ )[
+ + 证明:$O$ 在平面 $A B C D$上;
+ + 求直线 $A C$ 与直线 $P O$ 所成角的余弦值.
+ ]
+]
+
+#question(points: 17, bottom: 2in)[
+ 设椭圆 $C: x^2 / a^2 + y^2 / b^2 = 1 (a > b > 0)$,记 $A$为椭圆下端点,$B$ 为右端点,$|A B| = sqrt(10)$,且椭圆 $C$
+ 的离心率为 $(2sqrt(2)) / 3$.
+ + 求椭圆的标准方程;
+ + 设点 $P(m, n)$.
+ + 若 $P$ 不在 $y$ 轴上,设 $R$ 是射线 $A P$ 上一点,$|A R| dot |A P| = 3$,用 $m, n$ 表示点 $RR$ 的坐标;
+ + 设直线$O Q$ 的斜率为 $k_1$,直线 $O P$ 的斜率为 $k_2$,若 $k_1 = 3k_2$,$M$为椭圆上一点,求 $|P M|$ 的最大值.
+]
+
+#question(points: 17)[
+ 设函数 $f(x) = 5cos x - cos 5x$.
+ + 求 $f(x)$ 在 $[0, pi / 4]$ 的最大值;
+ + 给定 $theta in (0, pi),a$ 为实数,证明:存在 $y in [a - theta, a + theta]$,使得 $cos y lt.slant cos theta$;
+ + 若存在 $phi$,使得对任意 $x$,都有 $5cos x - cos(5x + phi) lt.slant b$,求 $b$ 的最小值.
+]
+
+#show: setup.with(mode: SOLUTION, show-answer: true)
+#let answer = tag.with(prefix: "答案:", suffix: [ \ ], color: maroon)
+
+#title[参考答案]
+
+#solution(title: "解析")[
+ #answer[A]
+ 解: #lorem(6)#score(6)
+]
+
+#solution[
+ #answer[B]
+ 解: #lorem(8)#score(8)
+]
diff --git a/packages/preview/ezexam/0.2.8/thumbnail.png b/packages/preview/ezexam/0.2.8/thumbnail.png
new file mode 100644
index 0000000000..e31a8192c8
Binary files /dev/null and b/packages/preview/ezexam/0.2.8/thumbnail.png differ
diff --git a/packages/preview/ezexam/0.2.8/typst.toml b/packages/preview/ezexam/0.2.8/typst.toml
new file mode 100644
index 0000000000..20ff1e3123
--- /dev/null
+++ b/packages/preview/ezexam/0.2.8/typst.toml
@@ -0,0 +1,17 @@
+[package]
+name = "ezexam"
+version = "0.2.8"
+entrypoint = "ezexam.typ"
+homepage = "https://ezexam.pages.dev/"
+authors = ["gbchu "]
+license = "MIT"
+description = "An exam template inspired by the LaTeX package exam-zh and also can make handouts"
+repository = "https://github.com/gbchu/ezexam.git"
+keywords = ["test", "exam", "exam-zh", "handouts", "讲义", "考试", "试卷"]
+compiler = "0.14.0"
+categories = ["paper","text","layout"]
+
+[template]
+path = "template"
+entrypoint = "main.typ"
+thumbnail = "thumbnail.png"
\ No newline at end of file