diff --git a/packages/preview/v-exam/0.1.0/LICENSE b/packages/preview/v-exam/0.1.0/LICENSE new file mode 100644 index 0000000000..45c09ad119 --- /dev/null +++ b/packages/preview/v-exam/0.1.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 KieuQuangVu-NCP + +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/v-exam/0.1.0/README.md b/packages/preview/v-exam/0.1.0/README.md new file mode 100644 index 0000000000..df69c23904 --- /dev/null +++ b/packages/preview/v-exam/0.1.0/README.md @@ -0,0 +1,215 @@ +# v-exam: 0.1.0 + +Bộ công cụ biên soạn bài tập, tài liệu ôn tập và trộn đề thi trắc nghiệm / tự luận chuẩn định dạng Bộ Giáo dục & Đào tạo Việt Nam dành cho mọi môn học (Toán, Vật lý, Hóa học, Sinh học, Lịch sử, Địa lý, GDKT&PL, ...). + +--- + +## 🚀 Tính năng nổi bật + +- **Tự động trộn đề:** Trộn đảo phương án NLC, đảo ý $a, b, c, d$ của câu hỏi Đúng/Sai, hoán vị câu hỏi theo seed mã đề. +- **Tự động xuất bảng đáp án & mã QR:** Tạo bảng đáp án tổng hợp và mã QR cho phần mềm chấm thi tự động (như UNT, Chấm thi KC, ...). +- **Hỗ trợ câu hỏi chùm (Dữ kiện chung):** Tự động gom nhóm các câu hỏi con đi kèm đoạn văn cảnh/dữ kiện dùng chung mà không bị xáo rời rạc cho trường hợp câu hỏi chùm các môn. +- **Biên soạn bài tập nối tiếp (`exercise` / `bai-tap-in-line`):** Hỗ trợ biên soạn tài liệu giảng dạy, bài tập ôn tập chuyên đề có kèm lời giải chi tiết. + +--- + +## 🖼️ Mẫu kết quả (Gallery) + +

+ Đề thi mẫu
+ Bảng đáp án
+ Mã QRcode dùng cho máy chấm +

+ +--- + +## 📦 Hướng dẫn sử dụng + +### 1. Import thư viện + +```typst +#import "@preview/v-exam:0.1.0": * +``` + +### 2. Định dạng ngân hàng câu hỏi + +Ngân hàng câu hỏi được lưu dưới dạng mảng `array` các `dictionary`: + +```typst +#let data = ( + // Câu hỏi đơn trắc nghiệm nhiều lựa chọn + ( + type: "NLC", + hv: none, // none: không có hình vẽ; hoặc image("đường dẫn"), canvas({}) + lv: 1, // lv: 1 - Nhận biết, 2 - Thông hiểu, 3 - Vận dụng + nd: [Nội dung lời dẫn], + pa: ([Phương án A], [Phương án B], [Phương án C], [Phương án D]), + cot: 4, // cot: 1, 2 hoặc 4 + da: 0, // da: 0 - A, 1 - B, 2 - C, 3 - D + lg: [Lời giải cho câu hỏi], + ), + // Câu hỏi chùm trắc nghiệm nhiều lựa chọn + ( + type: "NLC", + hv: none, + lv: 1, + is-chum: true, + du-kien: [Nội dung dùng chung], + cau-hoi-con: ( + ( + nd: [Nội dung lời dẫn câu hỏi 1], + pa: ([Phương án A], [Phương án B], [Phương án C], [Phương án D]), + cot: 4, + da: 0, + lg: [Lời giải cho câu hỏi 1], + ), + ( + nd: [Nội dung lời dẫn câu hỏi 2], + pa: ([Phương án A], [Phương án B], [Phương án C], [Phương án D]), + cot: 4, + da: 0, + lg: [Lời giải cho câu hỏi 2], + ), + ), + ), + // Câu hỏi đúng sai + ( + type: "TF", + hv: none, + lv: 2, + nd: [Lời dẫn của câu hỏi], + ytf: ( + [nội dung ý a], + [nội dung ý b], + [nội dung ý c], + [nội dung ý d], + ), + da: (1, 0, 1, 0), // 1 - đúng, 0 - sai + lg: ( + [Lời giải cho ý a], + [Lời giải cho ý b], + [Lời giải cho ý c], + [Lời giải cho ý d], + ), + ), + // Câu hỏi trả lời ngắn đơn + ( + type: "TLN", + hv: none, + lv: 2, + nd: [Lời dẫn của câu hỏi], + da: 1.25, + lg: [Lời giải cho bài toán], + dong_ke: 3, + ), + // Câu hỏi trả lời ngắn dạng chùm + ( + type: "TLN", + hv: none, + lv: 1, + is-chum: true, + du-kien: [Nội dung dùng chung], + cau-hoi-con: ( + ( + nd: [Nội dung lời dẫn câu hỏi 1], + da: 12, + lg: [Lời giải 1], + dong_ke: 2, + ), + ( + nd: [Nội dung lời dẫn câu hỏi 2], + da: 5, + lg: [Lời giải 2], + dong_ke: 2, + ), + ), + ), + // Câu hỏi tự luận + ( + type: "TL", + hv: none, + lv: 2, + nd: [Lời dẫn của câu hỏi], + lg: [Lời giải cho bài toán], + dong_ke: 5, + ), +) +``` + +### 3. Biên soạn bài tập / Tài liệu ôn tập (`exercise` / `bai-tap-in-line`) + +```typst +#import "@preview/v-exam:0.1.0": exercise, bai-tap-in-line +#import "file.typ" as b +#exercise( + b.data, + cm: 1, + num-nlc: (1, 0, 0), + num-tf: (0, 1, 0), + num-tln: (0, 1, 2), + show-lg: true, + seed: 123, + theo-lv: true, + dong-ke: 4, +) +``` + +### 4. Trộn đề thi + +#### a) Trộn đề thi từ ngân hàng (`make-exam-matrix` / `tron-de-bank-level`) + +Tạo các đề thi bằng cách rút ngẫu nhiên câu hỏi từ ngân hàng đề: + +```typst +#import "@preview/v-exam:0.1.0": make-exam-matrix, tron-de-bank-level +#import "file.typ" as b +#let matrix = ( + ( + bank: b.data, + nlc-dem: (1, 0, 0), + tf-dem: (0, 1, 0), + tln-dem: (0, 1, 0), + tl-dem: (0, 1, 0), + ), +) + +#let info = ( + so-gd: [SỞ GD VÀ ĐT QUẢNG NGÃI], + truong: [TRƯỜNG THPT NGHĨA HÀNH], + ky-thi: [KIỂM TRA GIỮA KỲ I], + mon: [VẬT LÝ 12], + thoi-gian: [45 phút], + ds-ma-de: ("101", "102", "103", "104"), +) + +// Gọi lệnh tạo đề: +#make-exam-matrix(matrix, info, show-lg: false, hien-thi-bang-dap-an: true, theo-lv: true) +// Hoặc alias tiếng Việt: +#tron-de-bank-level(matrix, info, show-lg: false, hien-thi-bang-dap-an: true, theo-lv: true) +``` + +#### b) Trộn đề cùng nội dung (`make-exam-sync` / `tron-de-cung-noi-dung`) + +Cùng một bộ câu hỏi nhưng xáo trộn vị trí câu và phương án giữa các mã đề: + +```typst +#make-exam-sync(matrix, info, seed-goc: 2026, show-lg: false, hien-thi-bang-dap-an: true, theo-lv: false, xao-cau: true, xao-pa: true) +// Hoặc alias tiếng Việt: +#tron-de-cung-noi-dung(matrix, info, seed-goc: 2026, show-lg: false, hien-thi-bang-dap-an: true, theo-lv: false, xao-cau: true, xao-pa: true) +``` + +#### c) Trộn đề chỉ đảo ý/phương án (`make-exam-sub-only` / `tron-de-chi-y`) + +Cố định thứ tự câu hỏi, chỉ hoán vị phương án hoặc các ý $a, b, c, d$: + +```typst +#make-exam-sub-only(matrix, info, seed-goc: 2024, show-lg: false, hien-thi-bang-dap-an: true, tf-mode: "y-only", nlc-mode: "none", theo-lv: false) +// Hoặc alias tiếng Việt: +#tron-de-chi-y(matrix, info, seed-goc: 2024, show-lg: false, hien-thi-bang-dap-an: true, tf-mode: "y-only", nlc-mode: "none", theo-lv: false) +``` + +--- + +## 📜 Giấy phép + +Phát hành theo giấy phép [MIT License](LICENSE). diff --git a/packages/preview/v-exam/0.1.0/gallery/Picture-bangda.png b/packages/preview/v-exam/0.1.0/gallery/Picture-bangda.png new file mode 100644 index 0000000000..cf6dbebec9 Binary files /dev/null and b/packages/preview/v-exam/0.1.0/gallery/Picture-bangda.png differ diff --git a/packages/preview/v-exam/0.1.0/gallery/Picture-de.png b/packages/preview/v-exam/0.1.0/gallery/Picture-de.png new file mode 100644 index 0000000000..5743256389 Binary files /dev/null and b/packages/preview/v-exam/0.1.0/gallery/Picture-de.png differ diff --git a/packages/preview/v-exam/0.1.0/gallery/Picture-maQR.png b/packages/preview/v-exam/0.1.0/gallery/Picture-maQR.png new file mode 100644 index 0000000000..c2a4466697 Binary files /dev/null and b/packages/preview/v-exam/0.1.0/gallery/Picture-maQR.png differ diff --git a/packages/preview/v-exam/0.1.0/lib.typ b/packages/preview/v-exam/0.1.0/lib.typ new file mode 100644 index 0000000000..403efb6d3c --- /dev/null +++ b/packages/preview/v-exam/0.1.0/lib.typ @@ -0,0 +1,1439 @@ +#import "@preview/cetz:0.5.2" +#import "@preview/rustycure:0.2.0": qr-code + +// ========================================== +// 1. CÀI ĐẶT CHUNG & BẢNG MÀU THIẾT LẬP +// ========================================== +#let toan-setup(body, math-color: black, math-size: 1.2em, nmsize: 1em) = { + show math.frac: it => { + set text(fill: math-color, size: math-size) + math.display(it) + } + show math.equation: it => { + set text(size: nmsize) + it + } + show list: it => { + let any-frac = it.children.any(item => { + let r = repr(item.body) + r.contains("frac") and not r.contains("list(") + }) + if any-frac { + set list(spacing: 1.1em) + it + } else { it } + } + body +} + +#let mau-sac = ( + lg : rgb("#a8f6f13c"), + duong: rgb("#0057b8"), + cau-pa: rgb("#1505f2"), + daT: rgb("#f3072a") +) + +#let to-abc(idx) = ("A", "B", "C", "D").at(idx) +#let to-ds(arr) = { + let res = "" + for v in arr { if v == 1 { res += "Đ" } else { res += "S" } } + res +} + +// ========================================== +// 2. THUẬT TOÁN BĂM & XÁO TRỘN ĐỀ +// ========================================== +#let ham-bam(index, seed) = { + let h = index * 1664525 + seed * 1013904223 + h = calc.rem(h, 4294967296) + h = calc.rem(h * 279470273, 4294967296) + return h +} + +#let tron-mang(arr, seed) = { + if arr.len() <= 1 { return arr } + let pairs = arr.enumerate().map((pair) => { + let (i, item) = pair + (ham-bam(i, seed), item) + }) + let n = pairs.len() + let sorted-pairs = () + let remaining = pairs + + for i in range(n) { + let min-idx = 0 + for j in range(1, remaining.len()) { + if remaining.at(j).at(0) < remaining.at(min-idx).at(0) { + min-idx = j + } + } + sorted-pairs.push(remaining.at(min-idx)) + remaining = remaining.slice(0, min-idx) + remaining.slice(min-idx + 1) + } + return sorted-pairs.map(p => p.at(1)) +} + +#let tron-mang-theo-lv(arr, seed) = { + if arr.len() <= 1 { return arr } + let get-lv(item) = { + if "lv" in item { item.lv } + else if "cau-hoi-con" in item and item.cau-hoi-con.len() > 0 and "lv" in item.cau-hoi-con.at(0) { item.cau-hoi-con.at(0).lv } + else { 1 } + } + + let nhom1 = arr.filter(q => get-lv(q) == 1) + let nhom2 = arr.filter(q => get-lv(q) == 2) + let nhom3 = arr.filter(q => get-lv(q) == 3) + let nhom-khac = arr.filter(q => { + let lv = get-lv(q) + lv != 1 and lv != 2 and lv != 3 + }) + + return tron-mang(nhom1, seed + 11) + tron-mang(nhom2, seed + 22) + tron-mang(nhom3, seed + 33) + tron-mang(nhom-khac, seed + 44) +} + +#let xao-theo-tuy-chon(arr, seed, theo-lv: false) = { + if theo-lv { + tron-mang-theo-lv(arr, seed) + } else { + tron-mang(arr, seed) + } +} + +// ========================================== +// 3. HÀM TẠO QR & HIỂN THỊ LỜI GIẢI +// ========================================== +#let tao-qr-code-key(results) = { + let all-content = "" + for entry in results { + let ma-de = str(entry.value.seed) + let part1 = entry.value.part1.join("") + let part2 = entry.value.part2.join("") + let part3 = entry.value.part3.map(str).join("|") + all-content += ma-de + "\n" + part1 + "\n" + part2 + "\n" + part3 + "\n" + } + return all-content +} + +#let hien-thi-lg(q, q-type, idx, seed, opt-indices: none, tf-indices: none) = { + toan-setup({ + v(-0.4em) + block( + width: 100%, + inset: (y: 0.5em, x: 0.5em, left: 8pt, right: 8pt), + fill: mau-sac.lg, + radius: (right: 4pt), + stroke: (left: 2pt + mau-sac.duong) + )[ + #v(0.2em) + #if q-type == "NLC" { + let correct-idx = if opt-indices != none { opt-indices.position(i => i == q.da) } else { q.da } + text(fill: blue.darken(100%), size: 11pt)[*Đáp án: #to-abc(correct-idx)*\ ] + text(fill: red.darken(0%), size: 15pt)[*Lời giải:*] + } else if q-type == "TF" or q-type == "tf" { + let correct-ans = if tf-indices != none { to-ds(tf-indices.map(i => q.da.at(i))) } else { to-ds(q.da) } + text(fill: blue.darken(100%), size: 11pt)[*Đáp án: #correct-ans*\ ] + text(fill: red.darken(0%), size: 15pt)[ *Lời giải:*] + } else if q-type == "TLN" { + text(fill: blue.darken(100%), size: 11pt)[*Kết quả: #q.da*] + text(fill: red.darken(0%), size: 15pt)[\ *Lời giải:*] + } else if q-type == "TL" { + text(fill: red.darken(0%), weight: "bold", size: 16pt)[*Lời giải:*] + } + + #if (q-type == "TF" or q-type == "tf") and type(q.lg) == array { + let indices = if tf-indices != none { tf-indices } else { range(q.lg.len()) } + let labels = ("a)", "b)", "c)", "d)") + + list( + marker: none, + spacing: 0.7em, + ..indices.enumerate().map(((new-i, orig-i)) => { + let is-true = q.da.at(orig-i) == 1 + let status-str = if is-true { "Đúng" } else { "Sai" } + let lg-content = if orig-i < q.lg.len() { q.lg.at(orig-i) } else { [] } + + [#text(fill: mau-sac.cau-pa, weight: "bold")[#labels.at(new-i) - #status-str:] #text(fill: blue.darken(100%), style: "italic")[#lg-content]] + }) + ) + } else { + text(fill: blue.darken(100%), style: "italic")[#q.lg] + } + ] + v(-0.1em) + }) +} + +// ========================================== +// 4. BÀI TẬP INLINE / ÔN TẬP TÀI LIỆU +// ========================================== +#let render-sub-sol(q, type-str, show-lg) = { + if not show-lg { return } + v(-0.8em) + block( + width: 100%, + stroke: (left: 2.5pt + mau-sac.duong), + fill: mau-sac.lg, + inset: (x: 10pt, y: 8pt), + radius: (right: 4pt), + breakable: true + )[ + #set par(leading: 0.65em) + #if type-str == "NLC" [ + #text(fill: mau-sac.duong, weight: "bold")[► Lời giải: \ ] + #if "lg" in q and q.lg != none and q.lg != [] [ #q.lg ] else [ Chưa có lời giải.] + ] + #if type-str == "TF" [ + #text(fill: mau-sac.duong, weight: "bold")[► Đáp án:] #text(fill: mau-sac.daT, weight: "bold")[#to-ds(q.da)] \ + #if "lg" in q and q.lg != none and q.lg != [] [ + #if type(q.lg) == array [ + #let labels = ("a)", "b)", "c)", "d)") + #list( + marker: none, + spacing: 0.7em, + ..q.lg.enumerate().map(((i, lg-item)) => { + let is-true = q.da.at(i) == 1 + let status-str = if is-true { "Đúng" } else { "Sai" } + [#text(fill: mau-sac.cau-pa, weight: "bold")[#labels.at(i) #status-str]\ #lg-item] + }) + ) + ] else [ + #q.lg + ] + ] else [ Chưa có lời giải.] + ] + #if type-str == "TLN" [ + #text(fill: mau-sac.duong, weight: "bold")[► Kết quả:] #text(fill: mau-sac.daT, weight: "bold")[#q.da] \ + #if "lg" in q and q.lg != none and q.lg != [] [ #v(0.01em)#q.lg #v(0.2em)] else [ Chưa có lời giải.] + ] + #if type-str == "TL" [ + #text(fill: mau-sac.duong, weight: "bold")[► Lời giải chi tiết:] \ + #if "lg" in q and q.lg != none and q.lg != [] [ #q.lg ] else [ Chưa có lời giải.] + ] + ] +} + +#let bai-tap-in-line( + cm: 1, + bank-data, + title: none, + seed: 0, + num-nlc: none, + num-tf: none, + num-tln: none, + num-tl: none, + show-lg: false, + dong-ke: none, + co-nlc: none, + co-tf: none, + co-tln: none, + co-tl: none, + theo-lv: false, +) = { + let cfg-nlc = if num-nlc != none { num-nlc } else { co-nlc } + let cfg-tf = if num-tf != none { num-tf } else { co-tf } + let cfg-tln = if num-tln != none { num-tln } else { co-tln } + let cfg-tl = if num-tl != none { num-tl } else { co-tl } + + let actual-bank = if type(bank-data) == dictionary and "data" in bank-data { + bank-data.data + } else if type(bank-data) == module { + bank-data.data + } else { + bank-data + } + + let std-bank = actual-bank.map(q => { + if "cau-hoi-con" in q and type(q.cau-hoi-con) == array and q.cau-hoi-con.len() > 0 { + let is-c = q.at("is-chum", default: q.cau-hoi-con.len() > 1) + let dk = q.at("du-kien", default: none) + let q-type = q.at("type", default: q.cau-hoi-con.at(0).at("type", default: "NLC")) + let q-lv = q.at("lv", default: 1) + ( + type: q-type, + lv: q-lv, + is-chum: is-c, + du-kien: dk, + cau-hoi-con: q.cau-hoi-con + ) + } else { + ( + type: q.at("type", default: "NLC"), + lv: q.at("lv", default: 1), + is-chum: false, + du-kien: none, + cau-hoi-con: (q,) + ) + } + }) + + let cat-theo-so-cau-con(arr, req-count) = { + if req-count <= 0 or arr.len() == 0 { return () } + let res = () + let current-cnt = 0 + for item in arr { + let n = item.cau-hoi-con.len() + if current-cnt + n <= req-count or res.len() == 0 { + res.push(item) + current-cnt += n + } + if current-cnt >= req-count { break } + } + return res + } + + let rut-cau(q-type, dem-config, sub-seed) = { + let loc-ques = std-bank.filter(q => q.type == q-type) + if loc-ques.len() == 0 or dem-config == none { return () } + + let get-q-lv(item) = { + if "lv" in item { item.lv } + else if item.cau-hoi-con.len() > 0 and "lv" in item.cau-hoi-con.at(0) { item.cau-hoi-con.at(0).lv } + else { 1 } + } + + if type(dem-config) == int { + let processing = if seed == none { loc-ques } else { xao-theo-tuy-chon(loc-ques, seed + sub-seed, theo-lv: theo-lv) } + return cat-theo-so-cau-con(processing, dem-config) + } + + if type(dem-config) == array { + let nb-req = dem-config.at(0, default: 0) + let th-req = dem-config.at(1, default: 0) + let vd-req = dem-config.at(2, default: 0) + + let b-nb = loc-ques.filter(q => get-q-lv(q) == 1) + let b-th = loc-ques.filter(q => get-q-lv(q) == 2) + let b-vd = loc-ques.filter(q => get-q-lv(q) == 3) + + if seed != none { + b-nb = tron-mang(b-nb, seed + sub-seed + 11) + b-th = tron-mang(b-th, seed + sub-seed + 22) + b-vd = tron-mang(b-vd, seed + sub-seed + 33) + } + + return cat-theo-so-cau-con(b-nb, nb-req) + cat-theo-so-cau-con(b-th, th-req) + cat-theo-so-cau-con(b-vd, vd-req) + } + + if type(dem-config) == dictionary { + let don-config = dem-config.at("don", default: (0, 0, 0)) + let chum-req = dem-config.at("chum", default: 0) + + let bank-chum = loc-ques.filter(q => q.is-chum == true) + let bank-don = loc-ques.filter(q => q.is-chum != true) + + if seed != none { + bank-chum = tron-mang(bank-chum, seed + sub-seed + 55) + bank-don = tron-mang(bank-don, seed + sub-seed + 77) + } + + let res-chum = bank-chum.slice(0, calc.min(chum-req, bank-chum.len())) + + let res-don = () + if type(don-config) == int { + res-don = cat-theo-so-cau-con(bank-don, don-config) + } else if type(don-config) == array { + let nb-req = don-config.at(0, default: 0) + let th-req = don-config.at(1, default: 0) + let vd-req = don-config.at(2, default: 0) + + let b-nb = bank-don.filter(q => get-q-lv(q) == 1) + let b-th = bank-don.filter(q => get-q-lv(q) == 2) + let b-vd = bank-don.filter(q => get-q-lv(q) == 3) + + res-don = cat-theo-so-cau-con(b-nb, nb-req) + cat-theo-so-cau-con(b-th, th-req) + cat-theo-so-cau-con(b-vd, vd-req) + } + + return res-chum + res-don + } + + return loc-ques + } + + let ds-nlc = rut-cau("NLC", cfg-nlc, 107) + let ds-tf = rut-cau("TF", cfg-tf, 233) + let ds-tln = rut-cau("TLN", cfg-tln, 357) + let ds-tl = rut-cau("TL", cfg-tl, 491) + + toan-setup({ + if title != none and title != "" { + v(0.5em) + align(center)[#text(fill: mau-sac.duong, weight: "bold", size: 13pt)[#title]] + v(0.3em) + } + + // PHẦN I: NLC + if ds-nlc.len() > 0 { + set par(first-line-indent: 0pt) + v(-0.5em) + if cm == 1 { + text(fill: mau-sac.cau-pa, weight: "bold", size: 13pt)[► BÀI TẬP TRẮC NGHIỆM NHIỀU LỰA CHỌN] + } + let c-idx = 0 + for item in ds-nlc { + if item.du-kien != none and item.du-kien != [] { + let n-sub = item.cau-hoi-con.len() + let start-c = c-idx + 1 + let end-c = c-idx + n-sub + block(width: 100%, breakable: false)[ + #rect(width: 100%, fill: rgb("#f8f9fa"), stroke: (left: 2.5pt + mau-sac.cau-pa), inset: (x: 8pt, y: 6pt), radius: (right: 3pt))[ + #text(fill: mau-sac.cau-pa, weight: "bold")[Dữ kiện dùng cho từ Câu #start-c đến Câu #end-c:] \ + #v(0.1em) #item.du-kien + ] + ] + } + + for q in item.cau-hoi-con { + c-idx += 1 + v(-0.4em) + block(width: 100%, inset: (y: 0.3em), breakable: true)[ + #let has-img = "hv" in q and q.hv != none and q.hv != "" + #grid( + columns: if has-img { (1fr, auto) } else { (1fr,) }, + gutter: 3pt, + row-gutter: 8pt, + align: (left + top, center + top), + [ + #v(-0.4em) + #text(fill: mau-sac.cau-pa, weight: "bold")[Câu #c-idx.] #q.nd \ + #v(-0.3em) + + #let is-img-option = q.pa.any(opt => { + let r = repr(opt) + r.contains("image(") or r.contains("cetz") or r.contains("canvas") + }) + + #if is-img-option [ + #let n-cols = if "cot" in q { q.cot } else { 4 } + #grid( + columns: (1fr,) * n-cols, + column-gutter: 0.8em, + row-gutter: 0.5em, + align: center + horizon, + ..q.pa.enumerate().map(((i, v)) => { + let is-correct = show-lg and ("da" in q) and (i == q.da) + let label-text = [#h(1em)*#to-abc(i).*] + let formatted-label = if is-correct { underline(text(fill: mau-sac.daT, weight: "bold")[#label-text]) } else { text(fill: mau-sac.cau-pa)[#label-text] } + align(left)[#grid(columns:(auto,1fr), column-gutter:10pt, [#formatted-label], [#v])] + }) + ) + ] else [ + #let n-cols = if "cot" in q { q.cot } else { 4 } + #grid( + columns: (1fr,) * n-cols, + row-gutter: 0.95em, + column-gutter: 0.1em, + align: if n-cols > 1 { left + horizon } else { left }, + inset: (x: 1em), + ..q.pa.enumerate().map(((i, v)) => { + let is-correct = show-lg and ("da" in q) and (i == q.da) + let label-text = [*#to-abc(i).*] + let formatted-label = if is-correct { underline(text(fill: mau-sac.daT, weight: "bold")[#label-text]) } else { text(fill: mau-sac.cau-pa)[#label-text] } + box[#grid(columns: (auto, 1fr), gutter: 2pt, align: left, formatted-label, v)] + }) + ) #v(0.5em) + ] + ], + if has-img [ #if type(q.hv) == content { q.hv } else { image(q.hv, width: 4.5cm) } ] + ) + ] + v(-0.7em) + render-sub-sol(q, "NLC", show-lg) + } + } + } + + // PHẦN II: TF + if ds-tf.len() > 0 { + set par(first-line-indent: 0pt) + v(-0.1em) + if cm == 1 { text(fill: mau-sac.cau-pa, weight: "bold", size: 13pt)[► BÀI TẬP TRẢ LỜI ĐÚNG/SAI] } + let c-idx = 0 + for item in ds-tf { + if item.du-kien != none and item.du-kien != [] { + let n-sub = item.cau-hoi-con.len() + let start-c = c-idx + 1 + let end-c = c-idx + n-sub + block(width: 100%, breakable: false)[ + #rect(width: 100%, fill: rgb("#f8f9fa"), stroke: (left: 2.5pt + mau-sac.cau-pa), inset: (x: 8pt, y: 6pt), radius: (right: 3pt))[ + #text(fill: mau-sac.cau-pa, weight: "bold")[Dữ kiện dùng cho Câu #start-c đến Câu #end-c:] \ + #v(0.1em) #item.du-kien + ] + ] + } + for q in item.cau-hoi-con { + c-idx += 1 + v(-0.6em) + block(width: 100%, inset: (y: 0.3em), breakable: true)[ + #let has-img = "hv" in q and q.hv != none and q.hv != "" + #grid( + columns: if has-img { (1fr, auto) } else { (1fr,) }, + gutter: 10pt, + align: (left + top, center + top), + [ + #v(0.2em) + #text(fill: mau-sac.cau-pa, weight: "bold")[Câu #c-idx.] #q.nd \ + #list( + marker: none, + spacing: 0.7em, + ..q.ytf.enumerate().map(((i, sq)) => { + let label-text = [#("a)", "b)", "c)", "d)").at(i)] + let formatted-label = text(fill: mau-sac.cau-pa, weight: "bold")[#h(0.5em) #label-text] + let ans-str = if show-lg and ("da" in q) and type(q.da) == array and i < q.da.len() { + if q.da.at(i) == 1 { " [Đúng]" } else { " [Sai]" } + } else { "" } + [#formatted-label #sq #text(fill: mau-sac.daT, weight: "bold")[#ans-str]] + }) + ) + ], + if has-img [ #if type(q.hv) == content { q.hv } else { image(q.hv, width: 4.5cm) } ] + ) + ] + v(-0.8em) + render-sub-sol(q, "TF", show-lg) + } + } + } + + // PHẦN III: TLN + if ds-tln.len() > 0 { + set par(first-line-indent: 0pt) + v(-0.5em) + if cm == 1 { text(fill: mau-sac.cau-pa, weight: "bold", size: 13pt)[► BÀI TẬP TRẢ LỜI NGẮN] } + let c-idx = 0 + for item in ds-tln { + if item.du-kien != none and item.du-kien != [] { + let n-sub = item.cau-hoi-con.len() + let start-c = c-idx + 1 + let end-c = c-idx + n-sub + block(width: 100%, breakable: false)[ + #rect(width: 100%, fill: rgb("#f8f9fa"), stroke: (left: 2.5pt + mau-sac.cau-pa), inset: (x: 8pt, y: 6pt), radius: (right: 3pt))[ + #text(fill: mau-sac.cau-pa, weight: "bold", style: "italic")[Dữ kiện dùng cho Câu #start-c đến Câu #end-c:] \ + #v(0.1em) #text(style: "italic")[#item.du-kien] + ] + ] + } + for q in item.cau-hoi-con { + c-idx += 1 + v(-0.8em) + block(width: 100%, inset: (y: 0.3em), breakable: true)[ + #let has-img = "hv" in q and q.hv != none and q.hv != "" + #grid( + columns: if has-img { (1fr, auto) } else { (1fr,) }, + gutter: 10pt, + row-gutter: 10pt, + align: (left + top, center + horizon), + [ + #v(-0.1em) + #text(fill: mau-sac.cau-pa, weight: "bold")[Câu #c-idx.] #q.nd + #v(0.1em) + #if not show-lg [ + #grid( + columns: (auto, auto), + column-gutter: 8pt, + align: horizon, + [*Trả lời:*], + [#(for i in range(4) { box(width: 1.2em, height: 1.2em, stroke: 0.4pt + gray.darken(80%), radius: 1pt); h(2pt) })] + ) + #v(-0.4em) + #let num-lines = if dong-ke == none or dong-ke == 0 { none + } else if "dong-ke" in q and q.dong-ke != none { q.dong-ke + } else { dong-ke } + #if num-lines != none and type(num-lines) == int and num-lines > 0 { + v(0.3em) + for i in range(num-lines) { + v(0.4em) + line(length: 100%, stroke: (dash: "dotted", thickness: 0.75pt, paint: gray.darken(80%))) + } + } + ] + ], + if has-img [ #if type(q.hv) == content { q.hv } else { image(q.hv, width: 4.5cm) } #v(0.2em)] + ) + ] + v(-0.4em) + render-sub-sol(q, "TLN", show-lg) + } + } + } + + // PHẦN IV: TL + if ds-tl.len() > 0 { + set par(first-line-indent: 0pt) + v(-0.5em) + if cm == 1 { text(fill: mau-sac.cau-pa, weight: "bold", size: 11pt)[► BÀI TẬP TỰ LUẬN] } + let c-idx = 0 + for item in ds-tl { + for q in item.cau-hoi-con { + c-idx += 1 + v(-0.4em) + block(width: 100%, inset: (y: 0.3em), breakable: true)[ + #let has-img = "hv" in q and q.hv != none and q.hv != "" + #grid( + columns: if has-img { (1fr, auto) } else { (1fr,) }, + gutter: 10pt, + align: (left + top, center + top), + [ + #v(-0.4em) + #text(fill: mau-sac.cau-pa, weight: "bold")[Câu #c-idx.] #q.nd + #v(0.5em) + ], + if has-img [ #if type(q.hv) == content { q.hv } else { image(q.hv, width: 4.5cm) } #v(1em)] + ) + #v(-0.8em) + #if not show-lg [ + #let num-lines = if dong-ke == none or dong-ke == 0 { none + } else if "dong-ke" in q and q.dong-ke != none { q.dong-ke + } else { dong-ke } + #if num-lines != none and type(num-lines) == int and num-lines > 0 { + v(0.3em) + for i in range(num-lines) { + v(0.15em) + line(length: 100%, stroke: (dash: "dotted", thickness: 0.75pt, paint: gray.darken(80%))) + v(0.35em) + } + } + ] + #v(0.4em) + ] + v(-0.8em) + render-sub-sol(q, "TL", show-lg) + } + } + } + }) +} + +// ========================================== +// 5. TRỘN ĐỀ THI +// ========================================== +#let render-test( + ma-de, + q-bank, + matrix, + info, + is-first: false, + show-lg: false, + tf-mode: "full", + nlc-mode: "full", + theo-lv: false, +) = { + if not is-first { + context { counter(page).update(1) } + } + + let seed = int(ma-de) + + let raw-nlc = q-bank.filter(matrix.nlc-loc) + let xao-nlc = xao-theo-tuy-chon(raw-nlc, seed + 107, theo-lv: theo-lv) + let nlc-dem = calc.min(matrix.nlc-dem, xao-nlc.len()) + let s-nlc = xao-nlc.slice(0, nlc-dem) + + let raw-tf = q-bank.filter(matrix.tf-loc) + let xao-tf = xao-theo-tuy-chon(raw-tf, seed + 233, theo-lv: theo-lv) + let tf-dem = calc.min(matrix.tf-dem, xao-tf.len()) + let s-tf = xao-tf.slice(0, tf-dem) + + let raw-tln = q-bank.filter(matrix.tln-loc) + let xao-tln = xao-theo-tuy-chon(raw-tln, seed + 357, theo-lv: theo-lv) + let tln-dem = calc.min(matrix.tln-dem, xao-tln.len()) + let s-tln = xao-tln.slice(0, tln-dem) + + let raw-tl = q-bank.filter(matrix.tl-loc) + let xao-tl = xao-theo-tuy-chon(raw-tl, seed + 491, theo-lv: theo-lv) + let tl-dem = calc.min(matrix.tl-dem, xao-tl.len()) + let s-tl = xao-tl.slice(0, tl-dem) + + let part1-ans = () + let part2-ans = () + let part3-ans = () + let part4-ans = () + + toan-setup({ + set page(paper: "a4", margin: 1.2cm, footer: context [ + #set text(size: 9pt); *#h(1fr) Trang #counter(page).display() - Mã đề #ma-de* + ]) + + grid( + columns: (1.2fr, 1fr), + gutter: 10pt, + align(center)[ + #text(size: 11pt)[#info.so-gd] \ + #text(size: 11pt)[#strong(info.truong)] \ + #v(-0.5em) + #line(length: 40%, stroke: 0.5pt) + ], + align(center)[ + #text(size: 11pt)[#strong(info.ky-thi)] \ + #text(size: 11pt)[Môn: #strong[#info.mon]] \ + #text(size: 10pt)[Thời gian: #emph[#info.thoi-gian]] + ] + ) + grid(columns: (1fr, auto), + [Họ tên: ..............................................................................\ + #v(0.1em) + Lớp: ................ + Số báo danh: ....................... + Phòng thi: ............], + box(stroke: 0.8pt + black, inset: (x: 6pt, y: 6pt))[#strong[Mã đề: #ma-de]] + ) + line(length: 100%, stroke: 0.5pt) + + if s-nlc.len() > 0 { + let tong-so-cau = s-nlc.fold(0, (acc, item) => { + let n = if "cau-hoi-con" in item { item.cau-hoi-con.len() } else { 1 } + acc + n + }) + set par(first-line-indent: 0pt) + v(-0.1em) + text(fill: mau-sac.cau-pa)[*► Thí sinh trả lời từ câu 1 đến câu #tong-so-cau.* #emph[*Mỗi câu thí sinh chỉ được chọn một phương án.*]#v(0.2em)] + + let c-idx = 0 + for item in s-nlc { + let is-item-chum = "is-chum" in item and item.is-chum == true and "cau-hoi-con" in item and item.cau-hoi-con.len() > 1 + let ds-cau-can-ve = if "cau-hoi-con" in item { item.cau-hoi-con } else { (item,) } + + if is-item-chum { + let n-sub = ds-cau-can-ve.len() + let start-num = c-idx + 1 + let end-num = c-idx + n-sub + v(0.2em) + block(width: 100%, breakable: false)[ + #rect(width: 100%, fill: rgb("#f8f9fa"), stroke: (left: 2.5pt + mau-sac.cau-pa), inset: (x: 8pt, y: 6pt), radius: (right: 3pt))[ + #text(fill: mau-sac.cau-pa)[*Dữ kiện dùng cho từ Câu #start-num đến Câu #end-num:*] \ + #v(0.1em) #item.du-kien + ] + ] + } + + for q in ds-cau-can-ve { + let opt-indices = if seed == 0 or nlc-mode == "none" { range(4) } else { tron-mang(range(4), seed + c-idx * 73) } + let new-opts = opt-indices.map(i => q.pa.at(i)) + part1-ans.push(to-abc(opt-indices.position(i => i == q.da))) + + v(-0.2em) + block(width: 100%, inset: (y: 0.3em), breakable: true)[ + #let has-img = "hv" in q and q.hv != none and q.hv != "" + #grid( + columns: if has-img { (1fr, auto) } else { (1fr,) }, + gutter: 10pt, + align: (left + top, center + horizon), + [ + #v(-0.3em) + #text(fill: mau-sac.cau-pa)[*Câu #(c-idx + 1).*] #q.nd \ + #v(-0.3em) + #let is-img-option = new-opts.any(opt => { + let r = repr(opt) + r.contains("image(") or r.contains("cetz") or r.contains("canvas") + }) + #if is-img-option [ + #grid( + columns: (1fr, 1fr, 1fr, 1fr), + column-gutter: 0.8em, row-gutter: 0.5em, align: center + horizon, + ..new-opts.enumerate().map(((i, v)) => [#align(center)[#text(fill: mau-sac.cau-pa)[*#to-abc(i).*]) #v]]) + ) + ] else [ + #let n-cols = if "cot" in q { q.cot } else { 4 } + #grid( + columns: (1fr,) * n-cols, row-gutter: 0.6em, column-gutter: 1.2em, align: left + horizon, + ..new-opts.enumerate().map(((i, v)) => [#text(fill: mau-sac.cau-pa)[#h(1em)*#to-abc(i).*] #v]) + ) + ] + ], + if has-img [ #align(center + top)[#if type(q.hv) == content [ #q.hv ] else if type(q.hv) == str [ #image(q.hv, width: 4.5cm) ]] ] + ) + ] + v(-0.8em) + if show-lg and "lg" in q and q.lg != [] { hien-thi-lg(q, "NLC", c-idx, seed, opt-indices: opt-indices) } + v(-0.2em) + c-idx += 1 + } + } + } + + if s-tf.len() > 0 { + set par(first-line-indent: 0pt) + v(0.5em); text(mau-sac.cau-pa)[*► Thí sinh trả lời từ câu 1 đến câu #s-tf.len().* #emph[ *Trong mỗi ý a), b), c), d) của mỗi câu, thí sinh chọn đúng hoặc sai.*]] + for (idx, q-item) in s-tf.enumerate() { + let q = if "cau-hoi-con" in q-item { q-item.cau-hoi-con.at(0) } else { q-item } + let sub-indices = if tf-mode == "none" { range(4) } + else if tf-mode == "y-only" { + let content-str = repr(q.nd) + repr(q.ytf) + let content-hash = calc.rem(content-str.len() * 265435761, 123456789) + tron-mang(range(4), content-hash * 31 + int(ma-de) * 7919) + } else { tron-mang(range(4), seed + idx * 109 + int(ma-de) * 10000) } + let new-subs = sub-indices.map(i => q.ytf.at(i)) + part2-ans.push(to-ds(sub-indices.map(i => q.da.at(i)))) + v(-0.3em) + block(width: 100%, inset: (y: 0.3em), breakable: true)[ + #let has-img = "hv" in q and q.hv != none and q.hv != "" + #grid( + columns: if has-img { (1fr, auto) } else { (1fr,) }, gutter: 10pt, align: (left + top, center + horizon), + [ + #text(fill: mau-sac.cau-pa)[*Câu #(idx + 1).* ]#q.nd \ + #list(marker: none, spacing: 0.7em, ..new-subs.enumerate().map(((i, sq)) => [#h(0.5em) #text(fill: mau-sac.cau-pa)[#strong[#("a)", "b)", "c)", "d)").at(i)]] #sq])) + ], + if has-img [ #align(center + horizon)[#if type(q.hv) == content [ #q.hv ] else if type(q.hv) == str [ #image(q.hv, width: 4.5cm) ]] ] + ) + ] + v(-0.4em) + if show-lg and "lg" in q and q.lg != [] { hien-thi-lg(q, "TF", idx + s-nlc.len(), seed, tf-indices: sub-indices) } + v(-0.3em) + } + } + + if s-tln.len() > 0 { + let tong-so-cau = s-tln.fold(0, (acc, item) => { + let n = if "cau-hoi-con" in item { item.cau-hoi-con.len() } else { 1 } + acc + n + }) + set par(first-line-indent: 0pt) + v(0.5em); text(fill: mau-sac.cau-pa)[*► Thí sinh trả lời từ câu 1 đến câu #tong-so-cau.*] + let c-idx = 0 + for item in s-tln { + let is-item-chum = "is-chum" in item and item.is-chum == true and "cau-hoi-con" in item and item.cau-hoi-con.len() > 1 + let ds-cau-can-ve = if "cau-hoi-con" in item { item.cau-hoi-con } else { (item,) } + + if is-item-chum { + let n-sub = ds-cau-can-ve.len() + let start-num = c-idx + 1 + let end-num = c-idx + n-sub + v(0.2em) + block(width: 100%, breakable: false)[ + #rect(width: 100%, fill: rgb("#f8f9fa"), stroke: (left: 2.5pt + mau-sac.cau-pa), inset: (x: 8pt, y: 6pt), radius: (right: 3pt))[ + #text(fill: mau-sac.cau-pa)[*Dữ kiện dùng cho từ Câu #start-num đến Câu #end-num:*] \ + #v(0.1em) #item.du-kien + ] + ] + } + + for q in ds-cau-can-ve { + part3-ans.push(q.da) + v(-0.5em) + block(width: 100%, inset: (y: 0.3em), breakable: true)[ + #let has-img = "hv" in q and q.hv != none and q.hv != "" + #grid( + columns: if has-img { (1fr, auto) } else { (1fr,) }, gutter: 10pt, align: (left + top, center + horizon), + [ + #text(fill: mau-sac.cau-pa)[*Câu #(c-idx + 1).* ]#q.nd + #if not show-lg [ + #v(-0.4em) + #pad(left: 0em)[ + #grid(columns: (auto, auto), column-gutter: 10pt, align: (left + horizon, left + horizon), [*Trả lời:*], [#(for i in range(4) { box(width: 1.2em, height: 1.2em, stroke: 0.3pt); h(3pt) })]) + ] + ] + ], + if has-img [ #align(center + horizon)[#if type(q.hv) == content [ #q.hv ] else if type(q.hv) == str [ #image(q.hv, width: 4.5cm) ]] ] + ) + ] + v(-1em) + if show-lg and "lg" in q and q.lg != [] { hien-thi-lg(q, "TLN", c-idx + s-nlc.len() + s-tf.len(), seed) } + v(-0.2em) + c-idx += 1 + } + } + } + + if s-tl.len() > 0 { + set par(first-line-indent: 0pt) + v(0.3em); text(mau-sac.cau-pa)[*► TỰ LUẬN (#s-tl.len() câu)*] + v(-0.3em) + for (idx, q-item) in s-tl.enumerate() { + let q = if "cau-hoi-con" in q-item { q-item.cau-hoi-con.at(0) } else { q-item } + part4-ans.push((nd: q.nd, lg: q.lg)) + v(-0.3em) + block(width: 100%, inset: (y: 0.3em), breakable: true)[ + #let has-img = "hv" in q and q.hv != none and q.hv != "" + #grid( + columns: if has-img { (1fr, auto) } else { (1fr,) }, gutter: 10pt, align: (left + top, center + top), + [ #text(fill: mau-sac.cau-pa)[*Câu #(idx + 1).* ] #q.nd \ #v(-0.3em) ], + if has-img [ #align(center + top)[#if type(q.hv) == content [ #q.hv ] else if type(q.hv) == str [ #image(q.hv, width: 4.5cm) ]] ] + ) + #if show-lg and "lg" in q and q.lg != [] { hien-thi-lg(q, "TL", idx, seed) } + #v(-0.3em) + ] + } + } + + align(center)[#text(size: 11pt)[#strong[---- HẾT ----]]] + }) + + [#metadata(( + seed: ma-de, + part1: part1-ans, + part2: part2-ans, + part3: part3-ans, + part4: part4-ans + )) ] +} + +#let in-dap-an-tl(results) = { + context { + counter(page).update(1) + set page(footer: none) + } + toan-setup({ + align(center)[#text(20pt, weight: "bold")[HƯỚNG DẪN CHẤM TỰ LUẬN]] + v(-0.2em) + for entry in results { + let ma-de = str(entry.value.seed) + let lg-tl = entry.value.part4 + if lg-tl.len() > 0 { + align(left)[ + #block(fill: gray.lighten(80%), inset: 5pt, radius: 4pt)[ + #text(14pt, weight: "bold")[Mã đề: #ma-de] + ] + #v(-0.51em) + ] + for (idx, ans) in lg-tl.enumerate() { + align(left)[ + #text(12pt)[*Câu #(idx + 1). * #ans.nd] + #v(-0.5em) + #text(11pt, fill: blue.darken(40%))[*Lời giải:* ] + #v(-0.5em) + #block(inset: 12pt, fill: yellow.lighten(95%), radius: 4pt, width: 100%)[ + #text(fill: mau-sac.cau-pa, style: "italic")[#ans.lg] + ] + #v(-0.5em) + ] + } + } + } + }) +} + +#let rut-cau-level(raw-bank, q-type, dem-config, seed) = { + let actual-bank = if type(raw-bank) == dictionary and "data" in raw-bank { raw-bank.data } + else if type(raw-bank) == module { raw-bank.data } else { raw-bank } + + let std-bank = actual-bank.map(q => { + if "cau-hoi-con" in q and type(q.cau-hoi-con) == array and q.cau-hoi-con.len() > 0 { + let is-c = q.at("is-chum", default: q.cau-hoi-con.len() > 1) + let dk = q.at("du-kien", default: none) + let q-tp = q.at("type", default: q.cau-hoi-con.at(0).at("type", default: "NLC")) + let q-lv = q.at("lv", default: 1) + ( + type: q-tp, + lv: q-lv, + is-chum: is-c, + du-kien: dk, + cau-hoi-con: q.cau-hoi-con + ) + } else { + ( + type: q.at("type", default: "NLC"), + lv: q.at("lv", default: 1), + is-chum: false, + du-kien: none, + cau-hoi-con: (q,) + ) + } + }) + + let loc-ques = std-bank.filter(q => q.type == q-type) + if loc-ques.len() == 0 { return () } + + let bank-chum = loc-ques.filter(q => q.is-chum == true) + let bank-don = loc-ques.filter(q => q.is-chum != true) + + let cat-theo-so-cau-con(arr, req-count) = { + if req-count <= 0 or arr.len() == 0 { return () } + let res = () + let current-cnt = 0 + for item in arr { + let n = item.cau-hoi-con.len() + if current-cnt + n <= req-count or res.len() == 0 { + res.push(item) + current-cnt += n + } + if current-cnt >= req-count { break } + } + return res + } + + let xao-chum = tron-mang(bank-chum, seed) + let xao-don = tron-mang(bank-don, seed + 101) + + let nb-req = 0 + let th-req = 0 + let vd-req = 0 + let chum-req = 0 + + if type(dem-config) == array { + nb-req = dem-config.at(0, default: 0) + th-req = dem-config.at(1, default: 0) + vd-req = dem-config.at(2, default: 0) + } else if type(dem-config) == dictionary { + let don-arr = dem-config.at("don", default: (0, 0, 0)) + if type(don-arr) == array { + nb-req = don-arr.at(0, default: 0) + th-req = don-arr.at(1, default: 0) + vd-req = don-arr.at(2, default: 0) + } else if type(don-arr) == int { + nb-req = don-arr + } + chum-req = dem-config.at("chum", default: 0) + } else if type(dem-config) == int { + return cat-theo-so-cau-con(tron-mang(loc-ques, seed), dem-config) + } + + let res-chum = cat-theo-so-cau-con(xao-chum, chum-req) + + let b-nb = xao-don.filter(q => q.lv == 1) + let b-th = xao-don.filter(q => q.lv == 2) + let b-vd = xao-don.filter(q => q.lv == 3) + + let res-don = cat-theo-so-cau-con(b-nb, nb-req) + cat-theo-so-cau-con(b-th, th-req) + cat-theo-so-cau-con(b-vd, vd-req) + + return res-chum + res-don +} + +#let tron-de-bank-level(banks-matrix, info, show-lg: false, hien-thi-bang-dap-an: true, theo-lv: false) = { + for (i, ma-de) in info.ds-ma-de.enumerate() { + let is-first = (i == 0) + let base-seed = int(ma-de) + + let rut-nlc = () + let rut-tf = () + let rut-tln = () + let rut-tl = () + + for (b-idx, item) in banks-matrix.enumerate() { + let b = item.bank + let nlc-seed = base-seed * 999999 + b-idx * 999999 + 107 + let tf-seed = base-seed * 999999 + b-idx * 999999 + 233 + let tln-seed = base-seed * 999999 + b-idx * 999999 + 357 + let tl-seed = base-seed * 999999 + b-idx * 999999 + 491 + + if "nlc-dem" in item { rut-nlc += rut-cau-level(b, "NLC", item.nlc-dem, nlc-seed) } + if "tf-dem" in item { rut-tf += rut-cau-level(b, "TF", item.tf-dem, tf-seed) } + if "tln-dem" in item { rut-tln += rut-cau-level(b, "TLN", item.tln-dem, tln-seed) } + if "tl-dem" in item { rut-tl += rut-cau-level(b, "TL", item.tl-dem, tl-seed) } + } + + rut-nlc = xao-theo-tuy-chon(rut-nlc, base-seed + 991, theo-lv: theo-lv) + rut-tf = xao-theo-tuy-chon(rut-tf, base-seed + 997, theo-lv: theo-lv) + rut-tln = xao-theo-tuy-chon(rut-tln, base-seed + 1009, theo-lv: theo-lv) + rut-tl = xao-theo-tuy-chon(rut-tl, base-seed + 1013, theo-lv: theo-lv) + + let final-bank = rut-nlc + rut-tf + rut-tln + rut-tl + + let fake-matrix = ( + nlc-loc: q => q.type == "NLC", + nlc-dem: rut-nlc.len(), + tf-loc: q => q.type == "TF", + tf-dem: rut-tf.len(), + tln-loc: q => q.type == "TLN", + tln-dem: rut-tln.len(), + tl-loc: q => q.type == "TL", + tl-dem: rut-tl.len(), + ) + + render-test(ma-de, final-bank, fake-matrix, info, is-first: is-first, show-lg: show-lg, theo-lv: theo-lv) + + if i < info.ds-ma-de.len() - 1 { + pagebreak(weak: true) + } + } + + if hien-thi-bang-dap-an { + pagebreak() + context { counter(page).update(1); set page(footer: none) } + align(center)[#text(16pt, weight: "bold")[BẢNG ĐÁP ÁN TRẮC NGHIỆM TỔNG HỢP]] + v(-0.5em) + + context { + let results = query() + if results.len() > 0 { + let total-q = results.at(0).value.part1.len() + results.at(0).value.part2.len() + results.at(0).value.part3.len() + if total-q > 0 { + table( + columns: (auto, ..info.ds-ma-de.map(_ => 1fr)), + align: center + horizon, + stroke: 0.5pt, + fill: (x, y) => if y == 0 { gray.lighten(80%) }, + [*Câu*], ..info.ds-ma-de.map(m => [*#m*]), + ..range(total-q).map(i => { + let row = ([#(i+1)],) + for entry in results { + let p1 = entry.value.part1 + let p2 = entry.value.part2 + let p3 = entry.value.part3 + let ans = if i < p1.len() { p1.at(i, default: "") } + else if i < p1.len() + p2.len() { p2.at(i - p1.len(), default: "") } + else { p3.at(i - p1.len() - p2.len(), default: "") } + row += ([#ans],) + } + row + }).flatten() + ) + } + } + } + + context { + let results = query() + if results.any(r => r.value.part4.len() > 0) { + in-dap-an-tl(results) + } + } + + pagebreak() + context { counter(page).update(1); set page(footer: none) } + align(center)[#text(16pt, weight: "bold")[MÃ QRCODE ĐÁP ÁN TRẮC NGHIỆM]] + v(1em) + context { + let results = query() + let all-qrcode = tao-qr-code-key(results) + align(center)[ + #block(inset: 15pt, fill: white, radius: 8pt, stroke: gray.lighten(30%) + 1pt)[ + #text(size: 14pt, weight: "bold")[ĐÁP ÁN TRẮC NGHIỆM CÁC MÃ ĐỀ] \ + #v(5pt) + #text(size: 10pt)[Số lượng mã đề: #results.len()] \ + #qr-code(all-qrcode, dark-color: black, light-color: white, quiet-zone: true) \ + #text(size: 9pt, fill: blue.darken(20%))[(Đáp án trắc nghiệm dành cho Unt)] + ] + ] + } + } +} + +#let tron-de-chi-y( + banks-matrix, + info, + seed-goc: 2024, + show-lg: false, + hien-thi-bang-dap-an: true, + tf-mode: "y-only", + nlc-mode: "full", + theo-lv: false, +) = { + let rut-nlc = () + let rut-tf = () + let rut-tln = () + let rut-tl = () + + for (b-idx, item) in banks-matrix.enumerate() { + let b = item.bank + let nlc-seed = seed-goc * 999999 + b-idx * 999999 + 107 + let tf-seed = seed-goc * 999999 + b-idx * 999999 + 233 + let tln-seed = seed-goc * 999999 + b-idx * 999999 + 357 + let tl-seed = seed-goc * 999999 + b-idx * 999999 + 491 + + if "nlc-dem" in item { rut-nlc += rut-cau-level(b, "NLC", item.nlc-dem, nlc-seed) } + if "tf-dem" in item { rut-tf += rut-cau-level(b, "TF", item.tf-dem, tf-seed) } + if "tln-dem" in item { rut-tln += rut-cau-level(b, "TLN", item.tln-dem, tln-seed) } + if "tl-dem" in item { rut-tl += rut-cau-level(b, "TL", item.tl-dem, tl-seed) } + } + + rut-nlc = tron-mang(rut-nlc, seed-goc + 991) + rut-tf = tron-mang(rut-tf, seed-goc + 997) + rut-tln = tron-mang(rut-tln, seed-goc + 1009) + rut-tl = tron-mang(rut-tl, seed-goc + 1013) + + for (i, ma-de) in info.ds-ma-de.enumerate() { + let is-first = (i == 0) + let final-bank = rut-nlc + rut-tf + rut-tln + rut-tl + + let fake-matrix = ( + nlc-loc: q => q.type == "NLC", + nlc-dem: rut-nlc.len(), + tf-loc: q => q.type == "TF", + tf-dem: rut-tf.len(), + tln-loc: q => q.type == "TLN", + tln-dem: rut-tln.len(), + tl-loc: q => q.type == "TL", + tl-dem: rut-tl.len(), + ) + + render-test( + ma-de, + final-bank, + fake-matrix, + info, + is-first: is-first, + show-lg: show-lg, + tf-mode: "y-only", + nlc-mode: "none", + theo-lv: false, + ) + + if i < info.ds-ma-de.len() - 1 { + pagebreak(weak: true) + } + } + + if hien-thi-bang-dap-an { + pagebreak() + context { counter(page).update(1); set page(footer: none) } + align(center)[#text(16pt, weight: "bold")[BẢNG ĐÁP ÁN TRẮC NGHIỆM TỔNG HỢP]] + v(-0.5em) + + context { + let results = query() + if results.len() > 0 { + let total-q = results.at(0).value.part1.len() + results.at(0).value.part2.len() + results.at(0).value.part3.len() + if total-q > 0 { + table( + columns: (auto, ..info.ds-ma-de.map(_ => 1fr)), + align: center + horizon, + stroke: 0.5pt, + fill: (x, y) => if y == 0 { gray.lighten(80%) }, + [*Câu*], ..info.ds-ma-de.map(m => [*#m*]), + ..range(total-q).map(i => { + let row = ([#(i+1)],) + for entry in results { + let p1 = entry.value.part1 + let p2 = entry.value.part2 + let p3 = entry.value.part3 + let ans = if i < p1.len() { p1.at(i, default: "") } + else if i < p1.len() + p2.len() { p2.at(i - p1.len(), default: "") } + else { p3.at(i - p1.len() - p2.len(), default: "") } + row += ([#ans],) + } + row + }).flatten() + ) + } + } + } + + context { + let results = query() + if results.any(r => r.value.part4.len() > 0) { + in-dap-an-tl(results) + } + } + + pagebreak() + context { counter(page).update(1); set page(footer: none) } + align(center)[#text(16pt, weight: "bold")[MÃ QRCODE ĐÁP ÁN TRẮC NGHIỆM]] + v(1em) + context { + let results = query() + let all-qrcode = tao-qr-code-key(results) + align(center)[ + #block(inset: 15pt, fill: white, radius: 8pt, stroke: gray.lighten(30%) + 1pt)[ + #text(size: 14pt, weight: "bold")[ĐÁP ÁN TRẮC NGHIỆM CÁC MÃ ĐỀ] \ + #v(5pt) + #text(size: 10pt)[Số lượng mã đề: #results.len()] \ + #qr-code(all-qrcode, dark-color: black, light-color: white, quiet-zone: true) \ + #text(size: 9pt, fill: blue.darken(20%))[(Đáp án trắc nghiệm dành cho Unt)] + ] + ] + } + } +} + +#let tron-de-cung-noi-dung( + banks-matrix, + info, + seed-goc: 2024, + show-lg: false, + hien-thi-bang-dap-an: true, + xao-cau: true, + xao-pa: true, + theo-lv: false, +) = { + let rut-nlc = () + let rut-tf = () + let rut-tln = () + let rut-tl = () + + for (b-idx, item) in banks-matrix.enumerate() { + let b = item.bank + let nlc-seed = seed-goc * 999999 + b-idx * 999999 + 107 + let tf-seed = seed-goc * 999999 + b-idx * 999999 + 233 + let tln-seed = seed-goc * 999999 + b-idx * 999999 + 357 + let tl-seed = seed-goc * 999999 + b-idx * 999999 + 491 + + if "nlc-dem" in item { rut-nlc += rut-cau-level(b, "NLC", item.nlc-dem, nlc-seed) } + if "tf-dem" in item { rut-tf += rut-cau-level(b, "TF", item.tf-dem, tf-seed) } + if "tln-dem" in item { rut-tln += rut-cau-level(b, "TLN", item.tln-dem, tln-seed) } + if "tl-dem" in item { rut-tl += rut-cau-level(b, "TL", item.tl-dem, tl-seed) } + } + + rut-nlc = tron-mang(rut-nlc, seed-goc + 991) + rut-tf = tron-mang(rut-tf, seed-goc + 997) + rut-tln = tron-mang(rut-tln, seed-goc + 1009) + rut-tl = tron-mang(rut-tl, seed-goc + 1013) + + for (i, ma-de) in info.ds-ma-de.enumerate() { + let is-first = (i == 0) + let base-seed = int(ma-de) + + let final-nlc = if xao-cau { xao-theo-tuy-chon(rut-nlc, base-seed + 2001, theo-lv: theo-lv) } else { rut-nlc } + let final-tf = if xao-cau { xao-theo-tuy-chon(rut-tf, base-seed + 2011, theo-lv: theo-lv) } else { rut-tf } + let final-tln = if xao-cau { xao-theo-tuy-chon(rut-tln, base-seed + 2027, theo-lv: theo-lv) } else { rut-tln } + let final-tl = if xao-cau { xao-theo-tuy-chon(rut-tl, base-seed + 2039, theo-lv: theo-lv) } else { rut-tl } + + let final-bank = final-nlc + final-tf + final-tln + final-tl + + let fake-matrix = ( + nlc-loc: q => q.type == "NLC", + nlc-dem: final-nlc.len(), + tf-loc: q => q.type == "TF", + tf-dem: rut-tf.len(), + tln-loc: q => q.type == "TLN", + tln-dem: final-tln.len(), + tl-loc: q => q.type == "TL", + tl-dem: final-tl.len(), + ) + + render-test( + ma-de, + final-bank, + fake-matrix, + info, + is-first: is-first, + show-lg: show-lg, + theo-lv: theo-lv, + ) + + if i < info.ds-ma-de.len() - 1 { + pagebreak(weak: true) + } + } + + if hien-thi-bang-dap-an { + pagebreak() + context { counter(page).update(1); set page(footer: none) } + align(center)[#text(16pt, weight: "bold")[BẢNG ĐÁP ÁN TRẮC NGHIỆM TỔNG HỢP]] + v(-0.5em) + + context { + let results = query() + if results.len() > 0 { + let total-q = results.at(0).value.part1.len() + results.at(0).value.part2.len() + results.at(0).value.part3.len() + if total-q > 0 { + table( + columns: (auto, ..info.ds-ma-de.map(_ => 1fr)), + align: center + horizon, + stroke: 0.5pt, + fill: (x, y) => if y == 0 { gray.lighten(80%) }, + [*Câu*], ..info.ds-ma-de.map(m => [*#m*]), + ..range(total-q).map(i => { + let row = ([#(i+1)],) + for entry in results { + let p1 = entry.value.part1 + let p2 = entry.value.part2 + let p3 = entry.value.part3 + let ans = if i < p1.len() { p1.at(i, default: "") } + else if i < p1.len() + p2.len() { p2.at(i - p1.len(), default: "") } + else { p3.at(i - p1.len() - p2.len(), default: "") } + row += ([#ans],) + } + row + }).flatten() + ) + } + } + } + + context { + let results = query() + if results.any(r => r.value.part4.len() > 0) { + in-dap-an-tl(results) + } + } + + pagebreak() + context { counter(page).update(1); set page(footer: none) } + align(center)[#text(16pt, weight: "bold")[MÃ QRCODE ĐÁP ÁN TRẮC NGHIỆM]] + v(1em) + context { + let results = query() + let all-qrcode = tao-qr-code-key(results) + align(center)[ + #block(inset: 15pt, fill: white, radius: 8pt, stroke: gray.lighten(30%) + 1pt)[ + #text(size: 14pt, weight: "bold")[ĐÁP ÁN TRẮC NGHIỆM CÁC MÃ ĐỀ] \ + #v(5pt) + #text(size: 10pt)[Số lượng mã đề: #results.len()] \ + #qr-code(all-qrcode, dark-color: black, light-color: white, quiet-zone: true) \ + #text(size: 9pt, fill: blue.darken(20%))[(Đáp án trắc nghiệm dành cho Unt)] + ] + ] + } + } +} + +// ========================================== +// 6. CÁC HÀM TRANG TRÍ TIÊU ĐỀ +// ========================================== +#let td-bai(title, bsize: 18pt, clorfont: rgb("#0309a7"), bground: rgb("#00ffff3b"), cle: center) = { + block( + width: 100%, fill: bground, radius: 8pt, stroke: 0.5pt + rgb("#c405ef"), inset: (x: 25pt, y: 8pt), + align(cle)[#text(fill: clorfont, size: bsize, weight: "bold", font: "Times New Roman", title)] + ) +} + +#let dang(title, stt: "1", kieu: "Dạng") = [ + #grid( + columns: (auto, 1fr), align: center + horizon, column-gutter: -2pt, + box(fill: rgb("#72f0b1a2"), radius: 8pt, inset: (x: 10pt, y: 10pt), stroke: 2pt + rgb("#72f0b1a2"))[ + #stack(spacing: 3pt, text(weight: "bold", fill: rgb("#f92a01fd"), size: 1.4em)[#kieu #stt:]) + ], + box(fill: rgb("#6ff6e484"), width: 100%, radius: 8pt, inset: (x: 15pt, y: 10pt), stroke: 2pt + rgb("#6ff6e484"))[ + #text(weight: "bold", fill: rgb("#e90335f8"), size: 1.4em)[#title] + ] + ) + #v(0.5em) +] + +#let mau-cap1 = rgb("#1f02c3") +#let mau-cap2 = rgb("#0501f0") +#let mau-cap3 = rgb("#024905") + +#let tieu-de(noi-dung, cap: 1) = { + if cap == 1 { + block(width: 100%, below: 0.7em, inset: (x: 0pt, y: 8pt), fill: rgb("#f3b40659"), radius: 4pt)[ + #align(left)[#text(weight: "bold", size: 1.3em, fill: mau-cap2)[#noi-dung]] + #v(0.15em) + ] + } else if cap == 2 { + block(width: 100%, below: 0.5em, inset: (x: 0pt, y: 7pt), fill: rgb("#65e4eb36"), radius: 4pt)[ + #text(weight: "bold", size: 1.2em, fill: mau-cap2)[#noi-dung] + #v(0.1em) + ] + } else if cap == 3 { + block(width: 100%, below: 0.3em, inset: (x: 0pt, y: 8pt), fill: rgb("#9bf29525"), radius: 3pt)[ + #text(weight: "bold", size: 1.1em, fill: mau-cap3)[▸ #noi-dung] + ] + } else { + block(width: 100%, below: 0.3em, inset: (x: 0pt, y: 4pt))[ + #text(weight: "regular", size: 1.0em, fill: rgb("#424242"))[◦ #noi-dung] + ] + } + v(0.3em) +} + +#let chi-muc(title, tsize: 16pt) = { + set par(first-line-indent: 0pt) + block(width: 100%)[ + #box(fill: rgb("#00ffff2e"), radius: (top-left: 0pt, top-right: 8pt, bottom-left: 0pt, bottom-right: 0pt), stroke: 0.5pt + rgb("#ee05ea"), inset: (x: 4pt, y: 8pt))[ + #text(fill: rgb("#003B5C"), size: tsize, weight: "bold", font: "Times New Roman", title) + ] + #v(-15pt) + #line(length: 100%, stroke: 2pt + rgb("#FF0066")) + ] +} + +#let luu-y(title, body) = { + block(width: 100%, below: 0.1em, stroke: (paint: red, thickness: 1pt), inset: (x: 12pt, y: 10pt), radius: 4pt, fill: rgb("#99eee832"))[ + #text(weight: "bold", fill: blue.darken(40%))[#underline(title, offset: 3pt)] + #v(-0.3em) + #set text(fill: oklab(36.99%, -0.019, -0.229), style: "italic") + #body + ] + v(1em) +} + +#let ps(a, b) = text(size: 1.5em)[#math.display(math.frac([#a], [#b]))] + +// ========================================== +// 7. BÍ DANH TIẾNG ANH (API CHUẨN UNIVERSE) +// ========================================== +#let exercise = bai-tap-in-line +#let make-exam-matrix = tron-de-bank-level +#let make-exam-sync = tron-de-cung-noi-dung +#let make-exam-sub-only = tron-de-chi-y diff --git a/packages/preview/v-exam/0.1.0/typst.toml b/packages/preview/v-exam/0.1.0/typst.toml new file mode 100644 index 0000000000..e0a8c73e08 --- /dev/null +++ b/packages/preview/v-exam/0.1.0/typst.toml @@ -0,0 +1,11 @@ +[package] +name = "v-exam" +version = "0.1.0" +entrypoint = "lib.typ" +authors = ["Kieu Quang Vu (@KieuQuangVu-NCP)"] +license = "MIT" +description = "Bộ công cụ biên soạn bài tập và trộn đề thi trắc nghiệm, tự luận chuẩn Bộ GD&ĐT Việt Nam dành cho mọi môn học." +repository = "https://github.com/KieuQuangVu-NCP/v-exam" +keywords = ["exam", "test", "vietnam", "quiz", "education", "v-exam"] +categories = ["office", "layout"] +compiler = "0.11.0"