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(<title>) { + 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 { <chapter> } 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 <chapter>]) +} + +#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 <title>]) + 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 <https://github.com/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"