diff --git a/packages/preview/ezexam/0.1.8/LICENSE b/packages/preview/ezexam/0.1.8/LICENSE
new file mode 100644
index 0000000000..aa8cb8bfc4
--- /dev/null
+++ b/packages/preview/ezexam/0.1.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.1.8/README.md b/packages/preview/ezexam/0.1.8/README.md
new file mode 100644
index 0000000000..78aef19e09
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/README.md
@@ -0,0 +1,79 @@
+# `ezexam`
+## Introduction
+`This template is primarily designed to help Chinese primary, middle and high school teachers or students in creating exams or handouts.`
+
+[Online Documentation](https://ezexam.pages.dev/)
+
+
+## Changelog
+
+### 0 . 1 . 0
+
++ 初版发布
+
+### 0 . 1 . 1
+
++ 修复 `choices` 方法中,若选项为图片时,设置宽度为百分比时,图片宽度无效的问题
+
+### 0 . 1 . 2
+
++ 将 `secret` 修改为方法,可以自定义显示内容
+
++ 优化 `choices` 方法,当选项过长时,选项从第二行开始进行缩进。修复选项中既有文字又有图表时,标签和内容对不齐的问题
+
++ 将 `question` 方法的参数 `with-heading-label` 的默认值修改为 `false`
+
++ `explain` 方法新增参数 `show-number` 、修改参数 `title` 的默认值为 `none`,默认不显示
+
++ `setup` 方法新增参数 `enum-numbering`
+
+### 0 . 1 . 3
+
++ 优化 `choices` 方法
+
++ 将 `question` 方法的参数名 `points-separate-par` 修改为 `points-separate`
+
++ 增加英文完型填空、7选5题型的支持,让 `paren` 和 `fillin` 方法可以使用题号作为占位符。使用详情查看 [`paren`](https://ezexam.pages.dev/paren) 和 [`fillin`](https://ezexam.pages.dev/fillin) 方法
+
++ `setup` 方法新增参数 `heading-numbering`,`heading-hanging-indent`,`enum-spacing`,`enum-indent` 提供更多自定义设置
+
++ 修复 `question` 个数超过9个时,内容对不齐的问题
+
+### 0 . 1 . 4
+
++ 将 `LECTURE` 修改为 `HANDOUTS`,更加符合语义
+
++ 将 `explain` 方法名修改为 `solution`,更加符合语义
+
++ 修复当修改弥封线类型时,试卷最后一页没有更改的 `bug`
+
++ 添加水印功能,`setup` 方法新增参数 `watermark`,`watermark-size`,`watermark-color`,`watermark-font`,`watermark-rotate`
+
+### 0 . 1 . 5
+
++ 修复水印被图片遮挡的 `bug`
+
+### 0 . 1 . 6
+
++ 修复有序列表,内容带有 `box` 时,编号和内容对不齐的 `bug`
+
++ 新增化学方程式的单线桥、双线桥的支持;原子、离子结构示意图的支持。使用详情查看 [`化学相关`](https://ezexam.pages.dev/chem)
+
+### 0 . 1 . 7
+
++ 优化代码,确保 `heading-size` 只修改一级标题;并将其更名为 `h1-size`
+
++ 为 `title` 方法新增参数 `color`
+
++ 修复 `solution` 方法,当启用 `title` 时,如果解析内容过多,一页放不下,标题会跑到下一页的 `bug`;并将其参数 `above` 更名为 `top`;参数 `below` 更名为 `bottom`;统一参数名;添加参数 `padding-top`、`padding-bottom`
+
++ 去除 `question` 方法参数 `line-height`;该参数会影响题干之间的距离;该参数原本用于设置题目内容的行高,当题目中的公式比较高时,题号和题目内容会错位,这时可以通过该参数来微调。但是会造成内容每一行与行之间的间隔变大。可参考 [使用技巧](https://ezexam.pages.dev/tips) 代替;添加参数 `padding-top`、`padding-bottom`
+
++ 修复 `choices` 方法,调整其上下外边距导致选项之间的距离会跟着影响的 `bug`
+
+### 0 . 1 . 8
++ 为 `mode` 添加新值 `SOLUTION`,当答案解析独立于试题存在时,使用此值可快速统一格式
++ 优化 `choices` 方法;将其参数 `column` 更名为 `columns`,做到和官方的 `columns` 参数一致
++ 废弃 `inline-square` 方法,推荐使用内置的 `table` 方法
++ 修复 `color-box` 方法报错的 `bug`
++ 优化 `secret` 、`zh-arabic` 方法
\ No newline at end of file
diff --git a/packages/preview/ezexam/0.1.8/ezexam.typ b/packages/preview/ezexam/0.1.8/ezexam.typ
new file mode 100644
index 0000000000..6fd6c4e828
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/ezexam.typ
@@ -0,0 +1,258 @@
+#import "lib/tools.typ": *
+#import "lib/outline.typ": *
+#import "lib/choice.typ": *
+#import "lib/question.typ": answer, fillin, fillinn, paren, parenn, question, score, solution, 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-size: 11pt,
+ font: source-han,
+ font-math: source-han,
+ line-height: 2em,
+ par-spacing: 2em,
+ first-line-indent: 0em,
+ heading-numbering: auto,
+ heading-hanging-indent: auto,
+ h1-size: auto,
+ heading-font: hei-ti,
+ heading-color: luma(0%),
+ heading-top: 10pt,
+ heading-bottom: 15pt,
+ enum-numbering: "(1.i.a)",
+ enum-spacing: 2em,
+ enum-indent: 0pt,
+ watermark: none,
+ watermark-color: rgb("#f666"),
+ watermark-font: source-han,
+ 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 must be HANDOUTS or EXAM or SOLUTION")
+ mode-state.update(mode)
+ let _footer(label) = context {
+ assert(
+ type(label) in (str, function, none) or label == auto,
+ message: "expected str or function or none or auto, found " + str(type(label)),
+ )
+ if label == none { return }
+ let _label = label
+ if label == auto {
+ if mode == HANDOUTS {
+ _label = "1 ✏ 1"
+ } else {
+ let _prefix = [#subject-state.get()试题#if mode == SOLUTION [答案]]
+ _label = zh-arabic(prefix: _prefix)
+ }
+ }
+ // 如果传进来的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)
+ }
+ 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: -2em,
+ )[
+ #rotate(-90deg, origin: right + bottom)[
+ #_create-seal(dash: line-type, info: student-info, supplement: supplement)
+ ]
+ ]
+ return
+ }
+
+ #if (chapter-last-page-location).contains(current) {
+ _width = if page.flipped {
+ page.height
+ } else { page.width }
+ // 章节最后页的弥封线
+ place(dx: _width - page.margin - 2em, dy: 2em)[
+ #rotate(90deg, origin: left + top, _create-seal(dash: line-type, supplement: supplement))
+ ]
+ }
+ ]
+ }
+ let _background() = {
+ if paper.columns == 2 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 = "一、"
+ heading-hanging-indent = 2.3em
+ } else { heading-numbering = "1.1.1" }
+ }
+ set heading(numbering: heading-numbering, hanging-indent: heading-hanging-indent)
+ show heading: it => {
+ v(heading-top)
+ text(heading-color, font: heading-font, it)
+ v(heading-bottom)
+ }
+
+ show heading.where(level: 1): it => {
+ let size = h1-size
+ if size == auto {
+ if mode == HANDOUTS { size = text.size } else { size = 10.5pt }
+ }
+ text(size: size, it)
+ }
+ set enum(numbering: enum-numbering, spacing: enum-spacing, indent: enum-indent)
+ set table(stroke: .5pt, align: center)
+ set table.cell(align: horizon)
+
+ // 分段函数样式
+ set math.cases(gap: 1em)
+ // 显示方程编号
+ set math.equation(numbering: "(1)", supplement: [Eq -]) if mode == HANDOUTS
+ show math.equation: it => {
+ // features: 一些特殊符号的设置,如空集符号设置更加漂亮
+ set text(font: font-math, features: ("cv01",))
+ // 1. 行内样式默认块级显示样式; 2. 添加数学符号和中文之间间距
+ let space = h(.25em, weak: true)
+ space + math.display(it) + space
+ }
+
+ if show-answer {
+ answer-state.update(true)
+ answer-color-state.update(answer-color)
+ }
+
+ doc
+}
+
+
+
diff --git a/packages/preview/ezexam/0.1.8/lib/choice.typ b/packages/preview/ezexam/0.1.8/lib/choice.typ
new file mode 100644
index 0000000000..42b4150fc1
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/lib/choice.typ
@@ -0,0 +1,74 @@
+#let choices(
+ columns: auto,
+ c-gap: 0pt,
+ r-gap: 25pt,
+ indent: 0pt,
+ body-indent: 5pt,
+ top: 0pt,
+ bottom: 0pt,
+ label: "A.",
+ ..options,
+) = layout(container => {
+ // 使用layout获取当前父元素的宽度
+ let args-named = options.named()
+ assert(args-named.len() == 0, message: "choices no " + repr(args-named) + " parameters")
+
+ let arr = options.pos()
+ let choice-number = arr.len()
+ assert(choice-number > 0, message: "choices must have at least one option")
+
+ let max-width = 0pt
+ // 拼接选项并添加标签和间距;获取选项中最长的宽度
+ for index in range(choice-number) {
+ // 加[] 是为了将内容转为content,有可能在使用时直接传入整数
+ let choice = [#arr.at(index)]
+ let _choice-width = 0pt
+ // 选项为图片、表格的处理
+ if choice.func() in (image, table) {
+ // 当选项为图片时,设置百分比宽度使用mesure获取宽度时为0pt, 设置百分比宽度的处理
+ if choice.has("width") and choice.width.length == 0pt {
+ _choice-width = choice.width.ratio * container.width
+ }
+ arr.at(index) = grid(
+ columns: 2,
+ pad(left: indent, numbering(label, index + 1)), pad(left: body-indent, choice),
+ )
+ } else {
+ arr.at(index) = par(
+ hanging-indent: 1.5em,
+ h(indent) + numbering(label, index + 1) + h(body-indent) + choice,
+ )
+ }
+
+ if columns != auto { continue }
+ _choice-width += measure(arr.at(index)).width
+ max-width = calc.max(max-width, _choice-width)
+ }
+
+ let _columns = columns
+ // 如果未指定列数,则自动排列,默认4列
+ if columns == auto {
+ _columns = 4
+ let actual-occupied-width = max-width + c-gap
+ // 排成1行,选项之间的间距
+ let choice-gap = container.width / choice-number - actual-occupied-width
+ let min-gap = 0.15in
+ if choice-gap < min-gap {
+ _columns = 2
+ // 排成2行,选项之间的间距
+ choice-gap = choice-gap * 2 + actual-occupied-width
+ if choice-gap < min-gap { _columns = 1 }
+ }
+ }
+
+ v(top)
+ grid(
+ columns: _columns * (1fr,),
+ column-gutter: c-gap,
+ row-gutter: r-gap,
+ align: horizon,
+ ..arr
+ )
+ v(bottom)
+})
+
diff --git a/packages/preview/ezexam/0.1.8/lib/const-state.typ b/packages/preview/ezexam/0.1.8/lib/const-state.typ
new file mode 100644
index 0000000000..cf465d70c7
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/lib/const-state.typ
@@ -0,0 +1,27 @@
+#let a3 = (
+ paper: "a3",
+ margin: 1in,
+ columns: 2,
+ flipped: true,
+)
+#let a4 = (
+ paper: "a4",
+ margin: 1in,
+ columns: 1,
+ flipped: false,
+)
+
+#let NCMM = "New Computer Modern Math"
+#let source-han = (NCMM, "Source Han Serif", "SimSun")
+#let hei-ti = (NCMM, "SimHei")
+#let kai-ti = (NCMM, "KaiTi")
+
+//"exam": 试卷模式; "handouts": 讲义模式(默认);"solution":解析模式
+#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", "")
\ No newline at end of file
diff --git a/packages/preview/ezexam/0.1.8/lib/outline.typ b/packages/preview/ezexam/0.1.8/lib/outline.typ
new file mode 100644
index 0000000000..fbb4216a6d
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/lib/outline.typ
@@ -0,0 +1,133 @@
+#import "const-state.typ": *
+
+#let chapter(body) = {
+ pagebreak(weak: true)
+ counter("chapter").step()
+ set heading(numbering: _ => counter("chapter").display("一、"))
+ place(top, hide[= #body ])
+}
+
+#let title(
+ body,
+ size: 15pt,
+ weight: "bold",
+ font: source-han,
+ color: black,
+ position: center,
+ top: 0pt,
+ bottom: 18pt,
+) = {
+ v(top)
+ align(position, text(font: font, size, weight: weight, color)[#body ])
+ v(bottom)
+ counter(heading).update(0)
+ counter("question").update(0)
+}
+
+#let subject(body, size: 21.5pt, spacing: 1em, font: hei-ti, top: -20pt, 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: "SimHei", body))
+
+#let exam-type(type, prefix: "试卷类型: ") = place(top + right, text(
+ font: hei-ti,
+)[#prefix#type])
+
+#let exam-info(
+ info: (
+ 时间: "120分钟",
+ 满分: "150分",
+ ),
+ weight: 500,
+ font: hei-ti,
+ size: 11pt,
+ gap: 2em,
+ top: 0pt,
+ bottom: 0pt,
+) = {
+ assert(info.len() > 0, message: "info cannot be empty")
+ set align(center)
+ grid(
+ columns: info.len(),
+ gutter: gap,
+ inset: (top: top, bottom: bottom),
+ align: center + horizon,
+ ..for (key, value) in info {
+ (text(size: size, font: font, weight: weight)[#key: #value],)
+ }
+ )
+}
+
+#let scoring-box(x: 0pt, y: 0pt) = place(dx: x, dy: y, right + top)[
+ #table(
+ columns: (auto, 1.6cm),
+ inset: 8pt,
+ )[得分][][阅卷人]
+]
+
+#let score-box(x: 0pt, y: 0pt) = place(dx: x, dy: y, right + top)[
+ #table(
+ rows: (auto, 1.2cm),
+ inset: 8pt,
+ )[得分][#h(3em)]
+]
+
+#let notice(format: "1.", indent: 2em, hanging-indent: auto, ..children) = context {
+ text(font: hei-ti)[注意事项:]
+ set enum(numbering: format, indent: indent)
+ set par(hanging-indent: if hanging-indent == auto {
+ -indent - enum.body-indent - measure(format).width
+ } else { hanging-indent })
+ for value in children.pos() [+ #par(value)]
+}
+
+#let _create-seal(
+ dash: "dashed",
+ supplement: "",
+ info: (:),
+) = {
+ assert(type(info) == dictionary, message: "expected dictionary, found " + str(type(info)))
+ set par(spacing: 10pt)
+ set text(font: hei-ti, size: 12pt)
+ set align(center)
+ set grid(columns: 2, align: horizon, gutter: .5em)
+ if supplement != "" { 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: "",
+) = {
+ 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)
+}
+
diff --git a/packages/preview/ezexam/0.1.8/lib/question.typ b/packages/preview/ezexam/0.1.8/lib/question.typ
new file mode 100644
index 0000000000..f1cdb91eb1
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/lib/question.typ
@@ -0,0 +1,196 @@
+#import "const-state.typ": HANDOUTS, answer-color-state, answer-state, mode-state
+
+#let question(
+ body,
+ body-indent: .7em,
+ indent: 0pt,
+ label: auto,
+ label-color: black,
+ label-weight: "regular",
+ with-heading-label: false,
+ points: none,
+ points-separate: true,
+ points-prefix: "(",
+ points-suffix: "分)",
+ top: 0pt,
+ bottom: 0pt,
+ padding-top: 0pt,
+ padding-bottom: 0pt,
+) = {
+ // 分数设置
+ assert(type(points) == int or points == none, message: "points must be a int or none")
+ if points != none {
+ body = [#points-prefix#points#points-suffix #if points-separate [ \ ] #body]
+ }
+
+ // 格式化题号
+ counter("question").step()
+ let _format = context counter("question").display(num => {
+ let _label = label
+ if label == auto {
+ if mode-state.get() == HANDOUTS {
+ _label = "【1.1.1.1.1.1】"
+ } else {
+ _label = "1."
+ }
+ }
+
+ let _counter = counter("placeholder")
+ if _counter.get().first() < num {
+ _counter.step()
+ }
+
+ let arr = (num,)
+ if with-heading-label {
+ // 去除heading label数组中的0
+ arr = counter(heading).get().filter(item => item != 0) + arr
+ }
+ text(label-color, weight: label-weight, box(align(right, numbering(_label, ..arr)), width: 1.45em))
+ })
+
+ v(top - padding-top)
+ list(
+ marker: _format,
+ body-indent: body-indent,
+ indent: indent,
+ pad(top: padding-top, bottom: padding-bottom, body),
+ )
+ v(bottom)
+}
+
+#let _get-answer(body, placeholder, with-number, update) = context {
+ 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 paren(
+ body,
+ justify: false,
+ placeholder: "▴",
+ with-number: false,
+ update: false,
+) = {
+ let body = _get-answer(body, placeholder, with-number, update)
+ [#if justify { h(1fr) } (~~#upper(body)~~)]
+}
+
+// 填空的横线
+#let fillin(
+ body,
+ length: 1em,
+ placeholder: "▴",
+ with-number: false,
+ update: false,
+) = {
+ let body = _get-answer(body, placeholder, with-number, update)
+ let space = h(length)
+ $underline(#space#body#space)$
+}
+
+// 类似英文中的7选5题型专用语法糖
+#let parenn = paren.with(with-number: true, update: true)
+#let fillinn = fillin.with(with-number: true, update: true)
+
+// 图文混排(左文右图)
+#let text-figure(
+ text: "",
+ figure,
+ figure-x: 0pt,
+ figure-y: 0pt,
+ top: 0pt,
+ bottom: 0pt,
+) = grid(
+ columns: 2,
+ align: horizon,
+ inset: (
+ top: top,
+ bottom: bottom,
+ ),
+ text, move(dx: figure-x, dy: figure-y)[#box[#figure]],
+)
+
+#let solution(
+ body,
+ title: none,
+ title-size: 12pt,
+ title-weight: "bold",
+ 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,
+ top: 0pt,
+ bottom: 0pt,
+ padding-top: 0pt,
+ padding-bottom: 0pt,
+ inset: (rest: 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")
+ let _inset = (rest: 10pt, top: 20pt, bottom: 20pt) + inset
+ v(top)
+ block(
+ width: 100%,
+ breakable: breakable,
+ inset: _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,
+ ))
+ let _title-height = measure(title-box).height
+ place(
+ title-align,
+ dx: title-x,
+ dy: -_inset.top - _title-height / 2 + title-y,
+ )[#title-box]
+ }
+
+ // 解析题号的格式化
+ #counter("explain").step()
+ #let format(..item) = context () => {
+ numbering("1.", ..counter("explain").get())
+ }
+
+ #list(
+ marker: if show-number { format } else { none },
+ pad(top: padding-top, bottom: padding-bottom, text(color, body)),
+ )
+ ]
+ v(bottom)
+}
+
+// 解析的分值
+#let score(points, color: maroon, score-prefix: "", score-suffix: "分") = text(color)[
+ #box(width: 1fr, repeat($dot$))#score-prefix#points#score-suffix
+]
+
+#let answer(body, color: maroon) = par(text(weight: 700, color)[答案: #body])
+
+
+
diff --git a/packages/preview/ezexam/0.1.8/lib/tools.typ b/packages/preview/ezexam/0.1.8/lib/tools.typ
new file mode 100644
index 0000000000..5fd754bb9d
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/lib/tools.typ
@@ -0,0 +1,23 @@
+#import "const-state.typ": subject-state, kai-ti
+// 一种页码格式: "第x页(共xx页)
+#let zh-arabic(prefix: "", suffix: "") = (..nums) => {
+ let arr = nums.pos()
+ [#prefix 第#str(arr.at(0))页(共#str(arr.at(-1))页)#suffix]
+}
+
+#let multi = text(maroon)[(多选)]
+
+#let color-box(body, color: blue, dash: "dotted", radius: 3pt) = {
+ box(
+ outset: .35em,
+ radius: radius,
+ stroke: (
+ thickness: .5pt,
+ dash: dash,
+ paint: color,
+ ),
+ text(font: kai-ti, color, body),
+ )
+ h(.8em)
+}
+
diff --git a/packages/preview/ezexam/0.1.8/template/17.png b/packages/preview/ezexam/0.1.8/template/17.png
new file mode 100644
index 0000000000..c8482216f1
Binary files /dev/null and b/packages/preview/ezexam/0.1.8/template/17.png differ
diff --git a/packages/preview/ezexam/0.1.8/template/6.png b/packages/preview/ezexam/0.1.8/template/6.png
new file mode 100644
index 0000000000..b5cfd3c8f6
Binary files /dev/null and b/packages/preview/ezexam/0.1.8/template/6.png differ
diff --git a/packages/preview/ezexam/0.1.8/template/main.typ b/packages/preview/ezexam/0.1.8/template/main.typ
new file mode 100644
index 0000000000..0ce9b5155b
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/template/main.typ
@@ -0,0 +1,173 @@
+#import "@preview/ezexam:0.1.8": *
+
+#show: setup.with(
+ // paper: a3,
+ mode: EXAM,
+)
+
+#outline()
+#chapter[2025新高考I卷]
+#title[2025新高考I卷]
+#subject[数学]
+#secret()
+
+#notice(
+ [答题前,请务必将自已的姓名、准考证号用0.5毫米黑色墨水的签字笔填写在试卷及答题卡的规定位置.],
+ [请认真核对监考员在答题卡上所粘贴的条形码上的姓名、准考证号与本人是否相符.],
+ [作答选择题必须用2B铅笔将答题卡上对应选项的方框涂满、涂黑;如需改动,请用橡皮擦干净后,再选涂其他答案.作答非选择题,必须用0.5毫米黑色墨水的签字笔在答题卡上的指定位置作答,在其他位置作答一律无效.],
+ [本试卷共4页,满分150分,考试时间为120分钟.考试结束后,请将本试卷和答题卡一并交回.],
+ )
+
+= 单选题:本题共 8 小题,每小题 5 分,共 40 分.在每小题给出的四个选项中,只有一项是符合题目要求的.
+#question[
+ $(1 + 5i)i$ 的虚部为 #paren[]
+ #choices([$-1$], [$0$], [$1$], [$6$])
+]
+
+#question[
+ 集合 $U = {x | x "为小于9的正整数" }$, $A = {1,3,5}$, 则 $complement_U A$ 中的元素个数为 #paren[]
+ #choices([$0$], [$3$], [$5$], [$8$])
+]
+
+#question[
+ 若双曲线 $C$ 的虚轴长为实轴长的 $sqrt(7)$ 倍,则 $C$ 的离心率为 #paren[]
+ #choices([$sqrt(2)$], [$2$], [$sqrt(7)$], [$2sqrt(2)$])
+]
+
+#question[
+ 若点 $(a,0) (a > 0)$ 是函数 $y = 2tan(x - pi / 3)$ 的图象的一个对称中心,则 $a$ 的最小值为 #paren[]
+ #choices([$30°$], [$60°$], [$90°$], [$135°$])
+]
+
+#question[
+ 设 $f(x)$ 是定义在 $RR$ 上且周期为 2 的偶函数,当 $2 lt.eq.slant x lt.eq.slant 3$ 时,$f(x) = 5 - 2x$,则
+ $f(-3 / 4 ) =$ #paren[]
+ #choices([$-1 / 2$], [$-1 / 4$], [$1 / 4$], [$1 / 2$])
+]
+
+#question[
+ 已知视风速是真风速和船风速的和向量,船风速与船行驶速度大小相等,方向相反.则真风速等级是 #paren[]
+ #text-figure(
+ text: choices(
+ [轻风 (1.6$~$3.3 m/s)],
+ [微风 (3.4$~$5.4 m/s)],
+ [和风 (5.5$~$7.8 m/s)],
+ [劲风 (8.0$~$10.7 m/s)],
+ ),
+ image("6.png", width: 50%),
+ )
+]
+
+#question[
+ 若圆 $x^2 + (y + 2)^2 = r^2 (r > 0)$ 上到直线 $y = sqrt(3)x + 2$ 的距离为 1 的点有且仅有 2 个,则 $r$ 的取值范围是
+ #paren[]
+ #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[]
+ #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[]
+ #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[]
+ #choices([$|A D| = |A F|$], [$|A E| = |A B|$], [$|A B| gt.eq.slant 6$], [$|A E| dot |B E| gt.eq.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[]
+ #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[].
+]
+
+#question[
+ 若一个正项等比数列的前 4 项和为 4,前 8 项和为 68,则该等比数列的公比为 #fillin[].
+]
+
+#question[
+ 一个箱子里有 5 个球,分别以 1$~$5 标号,若有放回取三次,记至少取出一次的球的个数 $X$,则 $E(X) =$#fillin[].
+]
+
+= 解答题:本题共 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(text: [ 附:$chi^2 = n(a d - b c)^2 / ((a + b)(c + d)(a + c)(b + d))$.], figure-x: 1in, table(
+ columns: 4,
+ [$P(chi^2 gt.eq.slant k)$], [0.005], [0.010], [0.001],
+ [$k$], [3.841], [6.635], [10.828],
+ ))
+]
+
+#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$.
+ #image("17.png", width: 30%)
+ + 证明:平面 $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$.
+ + 证明:$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, bottom: 2in)[
+ 设函数 $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.eq.slant cos theta$;
+ + 若存在 $phi$,使得对任意 $x$,都有 $5cos x - cos(5x + phi) lt.eq.slant b$,求 $b$ 的最小值.
+]
+
+
+#show: setup.with(mode: SOLUTION, show-answer: true)
+
+#title[参考答案及评分细则]
+
+#solution(title: "解析")[
+ #answer[A]
+ 解: #lorem(6)#score(6)
+]
+
+#solution[
+ #answer[B]
+ 解:
+]
+
+
+
+
diff --git a/packages/preview/ezexam/0.1.8/thumnail.png b/packages/preview/ezexam/0.1.8/thumnail.png
new file mode 100644
index 0000000000..6354f674ec
Binary files /dev/null and b/packages/preview/ezexam/0.1.8/thumnail.png differ
diff --git a/packages/preview/ezexam/0.1.8/typst.toml b/packages/preview/ezexam/0.1.8/typst.toml
new file mode 100644
index 0000000000..85f7ab6d51
--- /dev/null
+++ b/packages/preview/ezexam/0.1.8/typst.toml
@@ -0,0 +1,17 @@
+[package]
+name = "ezexam"
+version = "0.1.8"
+entrypoint = "ezexam.typ"
+homepage = "https://ezexam.pages.dev/"
+authors = ["gbchu "]
+license = "MIT"
+description = "A exam and handouts template inspired by the LaTeX package exam-zh."
+repository = "https://github.com/gbchu/ezexam.git"
+keywords = ["test", "exam", "exam-zh", "handouts", "讲义", "考试", "试卷"]
+compiler = "0.13.1"
+categories = ["report","paper"]
+
+[template]
+path = "template"
+entrypoint = "main.typ"
+thumbnail = "thumnail.png"