diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml new file mode 100644 index 0000000..72fcc1f --- /dev/null +++ b/.github/workflows/integration.yaml @@ -0,0 +1,65 @@ +name: Build Typst document (CI) +on: + push: + paths: + - '**/*.typ' + - '**/*.yaml' + - '**/*.toml' + workflow_dispatch: + paths: + - '**/*.typ' + - '**/*.yaml' + - '**/*.toml' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: typst-community/setup-typst@v3 + with: + version: latest + + - name: Delete samples + run: | + rm -rf ./examples/*.pdf + + - name: List Files + id: file-list + shell: bash + run: | + list=$(ls -1 ./examples/*.typ); + file_list=$(echo "$list" | sed ':a;N;$!ba;s/\n/ /g') + echo "File List: $file_list" + echo "file_list=$file_list" >> $GITHUB_OUTPUT + + - name: Compile Typst to PDF + uses: mkpoli/compile-typst-action@main + with: + source_paths: ${{ steps.file-list.outputs.file_list }} + root_path: '.' + + - name: Delete samples + run: | + rm -rf ./doc/*.pdf + + - name: Compile documentation to PDF + uses: mkpoli/compile-typst-action@main + with: + source_paths: "doc/manual.typ" + root_path: '.' + + - name: Upload examples PDF file + uses: actions/upload-artifact@v3 + with: + name: examples-g-exam + path: ./examples/*.pdf + + - name: Upload documentation PDF file + uses: actions/upload-artifact@v3 + with: + name: document-g-exam + path: ./doc/*.pdf \ No newline at end of file diff --git a/.gitignore b/.gitignore index e01aca0..d43080e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -g-exam.pdf \ No newline at end of file +/src/**/*.pdf +/doc/**/*.pdf +!/doc/manual.pdf \ No newline at end of file diff --git a/README.md b/README.md index 1c7ad40..998e059 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,89 @@ # g-exam -Template to create exams with header, school letterhead, grade chart, ... +This template provides a way to generate exams. You can create questions and sub-questions, header with information about the academic center, score box, subject, exam, header with student information, clarifications, solutions, watermark with information about the exam model and teacher. -## Features +#### Features - Scoreboard. - Scoring by questions and subquestions. - Student information, on the first page or on all odd pages. - Question and subcuestion. +- Show solutions and clarifications - List of clarifications. - Teacher's Watermark - Exam Model Watermark +## Usage -# Examples +For information, see the [manual](./doc/manual.pdf). -### Minimal Example - -``` typ -#import "@preview/g-exam:0.1.1": g-exam, g-question, g-subquestion +To use this package, simply add the following code to your document: -#show: g-exam.with( - #g-question(point: 2)[Question 1] - #v(1fr) - #g-question(point: 2)[Question 1] - #v(1fr) -) -``` +## Examples -### Minimal Example with sub-question +### Minimal Example -``` typst -#import "@preview/g-exam:0.1.1": g-exam, g-question, g-subquestion +```typ +#import "@preview/g-exam:0.3.0": * #show: g-exam.with( - #g-question[Question 1] - - #g-subquestion[Question 1] - #v(1fr) - - #g-subquestion[Question 1] - #v(1fr) - - #g-subquestion(point: 2)[Question 1] + #g-question(point: 2)[List prime numbers] #v(1fr) + + #g-question(point: 2)[Complete the following sentences] + #g-subquestion[Don Quixote was written by ...] + #v(1fr) + + #g-subquestion[The name of the continent we live on is ...] + #v(1fr) ) ``` + ### Full sample of an exam. 1. [Example of exam with punctuation](examples/exam-001.pdf) 1. [Example of exam with question only](examples/exam-002.pdf) 1. [Example of exam with subquestion](examples/exam-003.pdf) 1. [Example of exam with punctuation](examples/exam-005.pdf) + 1. [Example of exam with solution](examples/exam-005.pdf) +## Changelog -# Usage +### v0.3.0 -To use this package, simply add the following code to your document: +- Include parameter question-text-parameters. +- Show solution +- Expand documentation. +- Possibility of estrablecer question-point-position to none. +- Bug fix show watermark. + +### v0.2.0 -### g-exam - -Generate the skeleton of an exam, entering a header, student information, grade table, watermarks, ... - -#### Parameters of `g-exam` - - - **author**: - - **name**: Name of author of document. - - **email**: e-mail of author of document. - - **watermark**: Watermark with information about the author of the document. - - - **school**: - - **name**: Name of the school or institution generating the exam. - - **logo**: Logo of the school or institution generating the exam. - - - **exam-info**: - - **academic-period**: Academic period. - - **academic-level**: Academic level - - **academic-subject**: Academic subject. - - **number**: Number of exam. - - **content**: Content of exam. - - **model**: Watermark with information about the exam model. - - - **localization**: Location information, in case you want to change a parameter or your language is not supported. - - **grade-table-queston: Text question in grade table**. - - **grade-table-total: Text total in grade table**, - - **grade-table-points: Text points in grade table**, - - **grade-table-calification: Text calification in grade**, - - **point: Text point**, - - **points: Text points**. - - **page: Text page**, - - **page-counter-display**: Text page conter display. - - **family-name**: Text surname or family name in studen data. - - **personal-name**: Text name or personal name in studen data. - - **group**: Text gorup in studen data. - - **date**: Text date in studen data. - - **languaje**: (str) (en, es, de, fr, pt, it) Languages for Default Localization - - **decimal-separator*: (str) (".", ",") Decimal separator - - - **date**: Date of document. - - - **show-studen-data**: (none, str), - - **first-page**: Show studen data only in first page. - - **odd-pages**: Show studen data in all odd pages. - - `none`: Not show studen data. - - **question-point-position**: (none, left, right) - - **right**: Show question point on the right. - - **left**: Show question point on the left. - - `none`: Not show the question point. - - **show-grade-table**: (true, false) Show grade table, - - **clarifications**: (str, (:)) Text of clarifications for students. - - **body** (body): Body of exam. - -#### Parameters of `g-question` - - - **point**: (none, float) Points of the question. - - **body** (body): Body of question. - -#### Parameters of `g-subquestion` - - - **point**: (none, float) Points of the sub-question. - - **body** (body): Body of sub-question. - -# Changelog +- Control the size of the logo image. +- Convert to template +- Allow true and false values in show-studen-data. +- Show clarifications. +- Widen margin points. +- Show solution. + ### v0.1.1 - Fix loading image. -### v0.1.0 +### v0.1.1 -- Initial version submitted to typst/packages. +- Fix loading image. -# ToDo +### v0.1.0 +- Initial version submitted to typst/packages. - Multiple choice questions - Show solution of question. + +# CI + +Continuous integration status: + +[![.github/workflows/integration.yaml](https://github.com/MatheSchool/typst-g-exam/actions/workflows/integration.yaml/badge.svg)](https://github.com/MatheSchool/typst-g-exam/actions/workflows/integration.yaml) diff --git a/doc/example.typ b/doc/example.typ new file mode 100644 index 0000000..6310e84 --- /dev/null +++ b/doc/example.typ @@ -0,0 +1,75 @@ +#import "/src/lib.typ" + +// String that gets prefixed to every example code +// for compilation only! +#let example-preamble = "import cetz.draw: *;" +#let example-scope = (cetz: lib) + + +/// Render an example from a string +/// - source (string, raw): Example source code +/// - args (arguments): Arguments passed down to the canvas +/// - vertical (boolean): If true, show the code below the canvas +#let example(source, ..args, vertical: false) = { + if type(source) == content { + source = source.text + } + + let radius = .25cm + let border = 1pt + gray + let canvas-background = yellow.lighten(95%) + + let picture = lib.canvas( + eval( + example-preamble + source, + scope: example-scope + ), + ..args + ) + let source = box( + raw( + source, + lang: "typc" + ), + width: 100% + ) + + block( + if vertical { + align( + center, + stack( + dir: ttb, + spacing: 1em, + block( + width: 100%, + clip: true, + radius: radius, + stroke: border, + table( + columns: 1, + stroke: none, + fill: (c,r) => (canvas-background, white).at(r), + picture, + align(left, source) + ) + ), + ) + ) + } else { + block( + table( + columns: 2, + stroke: none, + fill: (canvas-background, white), + align: (center + horizon, left), + picture, + source + ), + width: 100%, + radius: radius, + clip: true, + stroke: border + ) + }, breakable: false) +} diff --git a/doc/logo.png b/doc/logo.png new file mode 100644 index 0000000..628aa70 Binary files /dev/null and b/doc/logo.png differ diff --git a/doc/manual.pdf b/doc/manual.pdf new file mode 100644 index 0000000..071b5e1 Binary files /dev/null and b/doc/manual.pdf differ diff --git a/doc/manual.typ b/doc/manual.typ new file mode 100644 index 0000000..1161eec --- /dev/null +++ b/doc/manual.typ @@ -0,0 +1,141 @@ +#import "@preview/tidy:0.2.0" + +#import "./util.typ": * +#import "./style.typ" as doc-style + +#import "../src/auxiliary.typ": * +#import "../src/g-exam.typ": * +#import "../src/g-question.typ": * +#import "../src/g-solution.typ": * + +// #import "./style.typ": +// https://github.com/Mc-Zen/tidy + +// // Usage: +// // ```example +// // /* canvas drawing code */ +// // ``` +// #show raw.where(lang: "example"): example +// #show raw.where(lang: "example-vertical"): example.with(vertical: true) + +#make-title() + +#set terms(indent: 1em) +#set par(justify: true) +#set heading(numbering: (..num) => if num.pos().len() < 4 { + numbering("1.1", ..num) + }) +#show link: set text(blue) + +// Outline +#{ + show heading: none + columns(2, outline(indent: true, depth: 3)) + pagebreak(weak: true) +} + +#set page(numbering: "1/1", header: align(right)[g-exam]) + += Introduction + +This template provides a way to generate exams. You can create questions and sub-questions, header with information about the academic center, score box, subject, exam, header with student information, clarifications, solutions, watermark with information about the exam model and teacher. + += Usage + +This is the minimum model for generating an exam, in which you define the g-exam template and the questions and subquestions with the g-question and g-subquestion commands. + +#pad(left: 1em)[ + +```typ +#import "@preview/g-exam:0.3.0": * +#show: g-exam.with() + +#g-question(point: 2)[List prime numbers] +#v(1fr) + +#g-question(point: 1)[Complete the following sentences] + #g-subquestion[Don Quixote was written by ...] + #v(1fr) + + #g-subquestion[The name of the continent we live on is ...] + #v(1fr) +```] + += Header + +#pad(left: 1em)[ +```typ +#show: g-exam.with( + author: ( + name: "Andrés Jorge Giménez Muñoz", + email: "matheschool@outlook.es", + watermark: "Teacher: andres", ), + school: ( + name: "Sunrise Secondary School", + logo: read("./logo.png", encoding: none), + ), + exam-info: ( + academic-period: "Academic year 2023/2024", + academic-level: "1st Secondary Education", + academic-subject: "Mathematics", + number: "2nd Assessment 1st Exam", + content: "Radicals and fractions", + model: "Model A" + ), +```] + += Information in the document's metadata + +If a pdf document is generated, the information will be saved in the document. Such as the author's name, e-mail, watermark, exam information, ... + +#pad(left: 1em)[ +```typ +#show: g-exam.with( + author: ( + name: "Andrés Jorge Giménez Muñoz", + email: "matheschool@outlook.es", + watermark: "Teacher: andres", ), + school: ( + name: "Sunrise Secondary School", + logo: read("./logo.png", encoding: none), + ), + exam-info: ( + academic-period: "Academic year 2023/2024", + academic-level: "1st Secondary Education", + academic-subject: "Mathematics", + number: "2nd Assessment 1st Exam", + content: "Radicals and fractions", + model: "Model A" + ), +```] + +This information can be consulted in the properties of the pdf document. + += Commands + +== Exam + +```example +/// User g-exam template +#show: g-exam.with() +``` + +#doc-style.parse-show-module("../src/g-exam.typ") + + + +The `g-exam` library has the `g-question`, `g-subquestion`, `g-solution` and `g-clarification` +commands to create questions, subquestions, solutions, and clarifications. + +== Questions and subquestions + +#doc-style.parse-show-module("../src/g-question.typ") + +== Solutions + +#doc-style.parse-show-module("../src/g-solution.typ") + +== Clarifications + +#doc-style.parse-show-module("../src/g-clarification.typ") + diff --git a/doc/style.typ b/doc/style.typ new file mode 100644 index 0000000..e05353f --- /dev/null +++ b/doc/style.typ @@ -0,0 +1,108 @@ +#import "example.typ": example +#import "/src/lib.typ" + +#import "@preview/tidy:0.2.0" +#import "@preview/t4t:0.3.2": is + +// #let show-function(fn, style-args) = { +// [ +// #heading(fn.name, level: style-args.first-heading-level + 1) +// #label(style-args.label-prefix + fn.name + "()") +// ] +// let description = if is.sequence(fn.description) { +// fn.description.children +// } else { +// (fn.description,) +// } +// let parameter-index = description.position(e => { +// e.func() == heading and e.body == [parameters] +// }) + +// description = description.map(e => if e.func() == heading { +// let fields = e.fields() +// let label = fields.remove("label", default: none) +// heading(offset: style-args.first-heading-level + 1, fields.remove("body"), ..fields); [#label] +// } else { e }) + +// if parameter-index != none { +// description.slice(0, parameter-index).join() +// } else { +// description.join() +// } + +// set heading(level: style-args.first-heading-level + 2) + +// block(breakable: style-args.break-param-descriptions, { +// heading("Parameters", level: style-args.first-heading-level + 2) +// (style-args.style.show-parameter-list)(fn, style-args.style.show-type) +// }) + +// for (name, info) in fn.args { +// let types = info.at("types", default: ()) +// let description = info.at("description", default: "") +// if description == [] and style-args.omit-empty-param-descriptions { continue } +// (style-args.style.show-parameter-block)( +// name, types, description, +// style-args, +// show-default: "default" in info, +// default: info.at("default", default: none), +// ) +// } + +// if parameter-index != none { +// description.slice(parameter-index+1).join() +// } +// } + +// #let show-parameter-block(name, types, content, show-default: true, default: none, in-tidy: false, ..a) = { +// if type(types) != array { +// types = (types,) +// } +// stack(dir: ttb, spacing: 1em, +// // name Default: +// block(breakable: false, width: 100%, stack(dir: ltr, +// [#text(weight: "bold", name + [:]) #types.map(tidy.styles.default.show-type).join(" or ")], +// if show-default { +// align(right)[ +// Default: #raw( +// lang: "typc", +// // Tidy gives defaults as strings but outside of tidy we pass defaults as the actual values +// if in-tidy { default } else { repr(default) } +// ) +// ] +// } +// )), +// // text +// block(inset: (left: .4cm), content) +// ) +// } + + +// #let show-type = tidy.styles.default.show-type +// #let show-outline = tidy.styles.default.show-outline +// #let show-parameter-list = tidy.styles.default.show-parameter-list + +// #let style = ( +// // show-function: show-function, +// // show-parameter-block: show-parameter-block.with(in-tidy: true), +// // show-type: show-type, +// // show-outline: show-outline, +// // show-parameter-list: show-parameter-list +// ) + +#let parse-show-module(path) = { + tidy.show-module( + tidy.parse-module( + read(path), + // scope: ( + // example: example, + // show-parameter-block: show-parameter-block, + // cetz: lib + // ) + ), + show-outline: false, + sort-functions: none, + // style: style + style: tidy.styles.default, + ) +} diff --git a/doc/util.typ b/doc/util.typ new file mode 100644 index 0000000..2f1b512 --- /dev/null +++ b/doc/util.typ @@ -0,0 +1,116 @@ +#import "/src/lib.typ" as g-exam + +/// Make the title-page +#let make-title() = { + let left-fringe = 39% + let left-color = blue.darken(30%) + let right-color = white + + let url = "https://github.com/MatheSchool/typst-g-exam" + let authors = ( + ([Andrés Jorge Giménez Muñoz], "andres.gimenez@outlook.com"), + ) + + set page( + numbering: none, + background: place( + top + left, + rect( + width: left-fringe, + height: 100%, + fill: left-color + ) + ), + margin: ( + left: left-fringe * 22cm, + top: 12% * 29cm + ), + header: none, + footer: none + ) + + set text(weight: "bold", left-color) + show link: set text(left-color) + + block( + place( + top + left, + dx: -left-fringe * 22cm + 5mm, + text(3cm, right-color)[g-exam] + ) + + text(29pt)[exam template for Typst] + ) + block( + v(1cm) + + text( + 20pt, + authors.map(v => link(v.at(1), [#v.at(0)])).join("\n") + ) + ) + block( + v(2cm) + + text( + 20pt, + link( + url, + [Version ] + [#g-exam.version] + ) + ) + ) + pagebreak(weak: true) +} + +/// Make chapter title-page +#let make-chapter-title(left-text, right-text, sub-title: none) = { + let left-fringe = 39% + let left-color = blue.darken(30%) + let right-color = white + + set page(numbering: none, background: { + place(top + left, rect(width: left-fringe, height: 100%, fill: left-color)) + }, margin: (left: left-fringe * 22cm, top: 12% * 29cm), header: none, footer: none) + + set text(weight: "bold", left-color) + show link: set text(left-color) + + block( + place(top + left, dx: -left-fringe * 22cm + 5mm, + text(3cm, right-color, left-text)) + + text(29pt, right-text)) + + block( + v(1cm) + + text(20pt, if sub-title != none { sub-title } else { [] })) + + pagebreak(weak: true) +} + + +#let def-arg(term, t, default: none, description) = { + if type(t) == str { + t = t.replace("?", "|none") + t = `<` + t.split("|").map(s => { + if s == "b" { + `boolean` + } else if s == "s" { + `string` + } else if s == "i" { + `integer` + } else if s == "f" { + `float` + } else if s == "c" { + `coordinate` + } else if s == "d" { + `dictionary` + } else if s == "a" { + `array` + } else if s == "n" { + `number` + } else { + raw(s) + } + }).join(`|`) + `>` + } + + stack(dir: ltr, [/ #term: #t \ #description], align(right, if default != none {[(default: #default)]})) +} diff --git a/examples/exam-001.pdf b/examples/exam-001.pdf deleted file mode 100644 index 9b50dc2..0000000 Binary files a/examples/exam-001.pdf and /dev/null differ diff --git a/examples/exam-002.pdf b/examples/exam-002.pdf index 946b866..fe5980d 100644 Binary files a/examples/exam-002.pdf and b/examples/exam-002.pdf differ diff --git a/examples/exam-002.typ b/examples/exam-002.typ index c51c277..7eac207 100644 --- a/examples/exam-002.typ +++ b/examples/exam-002.typ @@ -1,4 +1,4 @@ -#import "../g-exam.typ": g-exam, g-question, g-subquestion +#import "../src/lib.typ": g-exam, g-question, g-subquestion, g-solution #show: g-exam.with( author: ( @@ -12,7 +12,7 @@ ), exam-info: ( academic-period: "Academic year 2023/2024", - academic-level: "1º Secondary Education", + academic-level: "1st Secondary Education", academic-subject: "Mathematics", number: "2nd Assessment 1st Exam", content: "Radicals and fractions", diff --git a/examples/exam-003.pdf b/examples/exam-003.pdf index a02514a..85f1fe5 100644 Binary files a/examples/exam-003.pdf and b/examples/exam-003.pdf differ diff --git a/examples/exam-003.typ b/examples/exam-003.typ index 701ecbc..cd2a569 100644 --- a/examples/exam-003.typ +++ b/examples/exam-003.typ @@ -1,4 +1,4 @@ -#import "../g-exam.typ": g-exam, g-question, g-subquestion +#import "../src/lib.typ": g-exam, g-question, g-subquestion, g-solution #show: g-exam.with( author: ( @@ -8,11 +8,11 @@ ), school: ( name: "Sunrise Secondary School", - logo: read("./logo.png", encoding: none), + logo: image("./logo.png"), ), exam-info: ( academic-period: "Academic year 2023/2024", - academic-level: "1º Secondary Education", + academic-level: "1st Secondary Education", academic-subject: "Mathematics", number: "2nd Assessment 1st Exam", content: "Radicals and fractions", @@ -29,6 +29,9 @@ ) #g-question[Given the equation $x^n + y^n = z^n$ for $(x,y,z)$ and $n$ positive integers.] + +#image("./logo.png"), + #g-subquestion[For what values of $n$ is the statement in the previous question true?] #v(1fr) #g-subquestion[For $n=2$ there's a theorem with a special name. What's that name?] diff --git a/examples/exam-005.pdf b/examples/exam-005.pdf index 3080b3e..4d3a530 100644 Binary files a/examples/exam-005.pdf and b/examples/exam-005.pdf differ diff --git a/examples/exam-005.typ b/examples/exam-005.typ index 3df80ec..8f75215 100644 --- a/examples/exam-005.typ +++ b/examples/exam-005.typ @@ -1,4 +1,4 @@ -#import "../g-exam.typ": g-exam, g-question, g-subquestion +#import "../src/lib.typ": g-exam, g-question, g-subquestion, g-solution #show: g-exam.with( author: ( @@ -12,7 +12,7 @@ ), exam-info: ( academic-period: "Academic year 2023/2024", - academic-level: "1º Secondary Education", + academic-level: "1st Secondary Education", academic-subject: "Mathematics", number: "2nd Assessment 1st Exam", content: "Radicals and fractions", @@ -24,7 +24,7 @@ date: "November 21, 2023", show-studen-data: "first-page", show-grade-table: false, - question-point-position: left, + question-point-position: right, clarifications: "Answer the questions in the spaces provided. If you run out of room for an answer, continue on the back of the page." ) diff --git a/examples/exam-006.pdf b/examples/exam-006.pdf new file mode 100644 index 0000000..482c758 Binary files /dev/null and b/examples/exam-006.pdf differ diff --git a/examples/exam-006.typ b/examples/exam-006.typ new file mode 100644 index 0000000..38b9ad1 --- /dev/null +++ b/examples/exam-006.typ @@ -0,0 +1,61 @@ +#import "../src/lib.typ": g-exam, g-question, g-subquestion, g-solution + +#show: g-exam.with( + author: ( + name: "Andrés Jorge Giménez Muñoz", + email: "matheschool@outlook.es", + watermark: "Teacher: andres", + ), + school: ( + name: "Sunrise Secondary School", + logo: read("./logo.png", encoding: none), + ), + exam-info: ( + academic-period: "Academic year 2023/2024", + academic-level: "1st Secondary Education", + academic-subject: "Mathematics", + number: "2nd Assessment 1st Exam", + content: "Radicals and fractions", + model: "Model A" + ), + + languaje: "en", + decimal-separator: ",", + date: "November 21, 2023", + show-studen-data: "first-page", + show-grade-table: false, + question-point-position: right, + // show-solution: false, + clarifications: "Answer the questions in the spaces provided. If you run out of room for an answer, continue on the back of the page." +) + +#g-question[Given the equation $x^n + y^n = z^n$ for $(x,y,z)$ and $n$ positive integers.] +#g-subquestion(point: 10)[For what values of $n$ is the statement in the previous question true?] + +#g-solution( + alternative-content: v(1fr) + )[ + I know the demostration, but there's no room on the margin. For any clarification ask Andrew Whilst. +] + +#g-subquestion(point: 10)[For $n=2$ there's a theorem with a special name. What's that name? + + #g-solution( + alternative-content: v(1fr) + )[ + Pythagorean theorem. + ] +] + +#g-subquestion(point: 10)[What famous mathematician had an elegant proof for this theorem but +there was not enough space in the margin to write it down?]. +#v(1fr) + +#g-question(point: 20)[Prove that the real part of all non-trivial zeros of the function $zeta(z) "is" 1/2$]. + +#g-solution(alternative-content: [#v(1fr)] + )[ + I'm working on it. When I have the solution, I'll let you know.... \ + + #v(5pt) + ] diff --git a/examples/exam-localization.pdf b/examples/exam-localization.pdf new file mode 100644 index 0000000..cd21eba Binary files /dev/null and b/examples/exam-localization.pdf differ diff --git a/examples/exam-localization.typ b/examples/exam-localization.typ new file mode 100644 index 0000000..5874753 --- /dev/null +++ b/examples/exam-localization.typ @@ -0,0 +1,24 @@ +#import "../src/lib.typ": g-exam, g-question, g-subquestion, g-solution + +#show: g-exam.with( + localization: ( + grade-table-queston: [Number of *questions*], + grade-table-total: [Total _poinst_], + grade-table-points: [#text(fill: red)[Points]], + grade-table-calification: [#text(fill: gradient.radial(..color.map.rainbow))[Grades obtained]], + point: [point], + points: [Points], + page: [], + page-counter-display: "1 - 1", + family-name: "*Family* _name_", + given-name: "*Given* _name_", + group: [*Classroom*], + date: [*Date* of exam] + ), +) + +#g-question(point: 2)[Question 1] + +#g-question(point: 1)[Question 2] + +#g-question(point: 1.5)[Question 3] \ No newline at end of file diff --git a/examples/exam-001.typ b/examples/exam-mathematics.typ similarity index 80% rename from examples/exam-001.typ rename to examples/exam-mathematics.typ index 17258bf..d10c96c 100644 --- a/examples/exam-001.typ +++ b/examples/exam-mathematics.typ @@ -1,4 +1,4 @@ -#import "../g-exam.typ": g-exam, g-question, g-subquestion +#import "../src/lib.typ": g-exam, g-question, g-subquestion, g-solution #show: g-exam.with( author: ( @@ -12,26 +12,12 @@ ), exam-info: ( academic-period: "Academic year 2023/2024", - academic-level: "1º Secondary Education", + academic-level: "1st Secondary Education", academic-subject: "Mathematics", number: "2nd Assessment 1st Exam", content: "Radicals and fractions", model: "Model A" ), - // localization: ( - // grade-table-queston: none, - // grade-table-total: none, - // grade-table-points: none, - // grade-table-calification: none, - // point: none, - // points: none, - // page: none, - // page-counter-display: none, - // family-name: "Apellidos *4", - // personal-name: none, - // group: none, - // date: none - // ), languaje: "en", decimal-separator: ",", @@ -41,8 +27,6 @@ // show-studen-data: none, show-grade-table: true, question-point-position: right, - // question-point-position: left, - // question-point-position: none, clarifications: ( [This test must be performed with a blue or black non-erasable pen.], [Cheating, talking, getting up from the chair or disturbing the rest of the class can be reasons for withdrawal from the test, which will be valued with a zero.], @@ -52,10 +36,10 @@ #g-question(point: 2)[Calculate the following operations and simplify if possible: #g-subquestion[$display(5/12 dot 9/15=)$] - // #v(1fr) + #v(1fr) #g-subquestion[$display(10 dot 9/15=)$] - // #v(1fr) + #v(1fr) #g-subquestion[$display(5/12 : 4/15=)$] #v(1fr) diff --git a/examples/exam-minimal.pdf b/examples/exam-minimal.pdf index 69f26ea..8a787ae 100644 Binary files a/examples/exam-minimal.pdf and b/examples/exam-minimal.pdf differ diff --git a/examples/exam-minimal.typ b/examples/exam-minimal.typ index ad11482..fa2d1e1 100644 --- a/examples/exam-minimal.typ +++ b/examples/exam-minimal.typ @@ -1,4 +1,4 @@ -#import "../g-exam.typ": g-exam, g-question, g-subquestion +#import "../src/lib.typ": * #show: g-exam.with() diff --git a/examples/exam-points-position.pdf b/examples/exam-points-position.pdf new file mode 100644 index 0000000..4f7402e Binary files /dev/null and b/examples/exam-points-position.pdf differ diff --git a/examples/exam-points-position.typ b/examples/exam-points-position.typ new file mode 100644 index 0000000..e5f40d7 --- /dev/null +++ b/examples/exam-points-position.typ @@ -0,0 +1,102 @@ +#import "../src/lib.typ": g-exam, g-question, g-subquestion, g-solution +#import "@preview/cetz:0.2.1" + +#show: g-exam.with() + +#g-question(point: 2, point-position: right)[Question 1] + +#v(5cm) + +#g-question[ + Given the graphs of the following systems of linear equations, + Determines by the position of the lines the type of system according to the number of solutions. \ + +#columns(2, gutter: 2cm)[ + #g-subquestion(point: 0.5, point-position: left)[ + #align(center, + cetz.canvas(length: 0.7cm, { + cetz.plot.plot( + size: (10, 10), + axis-style: "school-book", + fill: "o" , + fill-below: true, + x-domain: (-5.2, 5.2), + y-domain: (-5.2, 5.2), + x-max: 5.2, + x-min:-5.2, + y-max: 5.2, + y-min:-5.2, + x-grid: "both", + y-grid: "both", + x-tick-step: 1, + y-tick-step: 1, + { + cetz.plot.add(((0,0),), mark-size: 0,) + + cetz.plot.add( + style: (stroke: blue + 2pt), + domain: (-5.2, 5.2), + x=>x + 3 + ) + + cetz.plot.add( + style: (stroke: olive + 2pt), + domain: (-5.2, 5.2), + x=>x + ) + } + ) + } + ) + ) + ] + #colbreak() + + #g-subquestion(point: 0.5, point-position: right)[ + #align(center, + cetz.canvas(length: 0.7cm, { + cetz.plot.plot( + size: (10, 10), + axis-style: "school-book", + fill: "o" , + fill-below: true, + x-domain: (-5.2, 5.2), + y-domain: (-5.2, 5.2), + x-max: 5.2, + x-min:-5.2, + y-max: 5.2, + y-min:-5.2, + x-grid: "both", + y-grid: "both", + x-tick-step: 1, + y-tick-step: 1, + { + cetz.plot.add(((0,0),), mark-size: 0,) + + cetz.plot.add( + style: (stroke: blue + 2pt), + domain: (-5.2, 5.2), + x=>-x - 4 + ) + + cetz.plot.add( + style: (stroke: olive + 2pt), + domain: (-5.2, 5.2), + x=>3 + ) + } + ) + } + ) + ) + ] +] +] + +#pagebreak() + +#g-question(point: 1)[Question 2] + +#g-question(point: 1.6, point-position: right)[Question 3] + +#g-question()[Question 4] \ No newline at end of file diff --git a/examples/exam-sugar-notation.pdf b/examples/exam-sugar-notation.pdf new file mode 100644 index 0000000..64deca5 Binary files /dev/null and b/examples/exam-sugar-notation.pdf differ diff --git a/examples/exam-sugar-notation.typ b/examples/exam-sugar-notation.typ new file mode 100644 index 0000000..8704868 --- /dev/null +++ b/examples/exam-sugar-notation.typ @@ -0,0 +1,37 @@ +#import "../src/lib.typ": * + +#show: g-exam.with() + +#g-question(point:.2)[Pregunta] + +#g-subquestion(point:.2)[sub 3] + += Title + +=? Question 1 + +=? 2.2 Question 2 + +=? 2 Question 6 + +==? Subquestion 3 + +==? 1.3 Subquestion 3 + +=% Clarification of question + +==? 1 Subquestion 4 + +=! Solution is this. + +=? .2 Question 33 + +=? Solve this ecuation $x^2 -4x +4 = 0$ + +#g-question(point:.2)[ Solve this ecuation $x^2 -4x +4 = 0$ ] + +=! Solulution of the question. + +=? 2.4 $x^2 -4x +4 = 0$ + +==? 2.4 $x^2 -4x +4 = 0$ diff --git a/examples/logo-1.png b/examples/logo-1.png new file mode 100644 index 0000000..628aa70 Binary files /dev/null and b/examples/logo-1.png differ diff --git a/examples/logo.png b/examples/logo.png index 628aa70..385a501 100644 Binary files a/examples/logo.png and b/examples/logo.png differ diff --git a/examples/size-text-question.pdf b/examples/size-text-question.pdf new file mode 100644 index 0000000..3141ab2 Binary files /dev/null and b/examples/size-text-question.pdf differ diff --git a/examples/size-text-question.typ b/examples/size-text-question.typ new file mode 100644 index 0000000..4f18a2b --- /dev/null +++ b/examples/size-text-question.typ @@ -0,0 +1,64 @@ +#import "../src/lib.typ": g-exam, g-question, g-subquestion, g-solution +// #set text(weight: "thin") + +#show: g-exam.with( + author: ( + name: "Andrés Jorge Giménez Muñoz", + email: "matheschool@outlook.es", + watermark: "Teacher: andres", + ), + school: ( + name: "Sunrise Secondary School", + logo: read("./logo.png", encoding: none), + ), + exam-info: ( + academic-period: "Academic year 2023/2024", + academic-level: "1st Secondary Education", + academic-subject: "Mathematics", + number: "2nd Assessment 1st Exam", + content: "Radicals and fractions", + model: "Model A" + ), + + languaje: "en", + decimal-separator: ",", + date: "November 21, 2023", + show-studen-data: "first-page", + show-grade-table: false, + question-point-position: right, + question-text-parameters: (size: 16pt, spacing:200%), + clarifications: "Answer the questions in the spaces provided. If you run out of room for an answer, continue on the back of the page." +) + +#g-question[#text(size:20pt)[Given] the equation $x^n + y^n = z^n$ for $(x,y,z)$ and $n$ positive integers.] +#g-subquestion(point: 10)[For what values of $n$ is the statement in the previous question true?] + +#g-solution( + alternative-content: v(1fr) + )[ + I know the demostration, but there's no room on the margin. For any clarification ask Andrew Whilst. +] + +#g-subquestion(point: 10)[For $n=2$ there's a theorem with a special name. What's that name? + + #g-solution( + alternative-content: v(1fr) + )[ + Pythagorean theorem. + ] +] + + +#g-subquestion(point: 10)[What famous mathematician had an elegant proof for this theorem but +there was not enough space in the margin to write it down?]. +// #v(1fr) + +#g-question(point: 20)[Prove that the real part of all non-trivial zeros of the function $zeta(z) "is" 1/2$]. + +#g-solution(alternative-content: [#v(1fr)] + )[ + I'm working on it. When I have the solution, I'll let you know.... \ + + #v(5pt) + ] + diff --git a/g-exam.typ b/g-exam.typ deleted file mode 100644 index 3bb98f1..0000000 --- a/g-exam.typ +++ /dev/null @@ -1,596 +0,0 @@ -#import "@preview/oxifmt:0.2.0": strfmt - -#let __question-number = counter("question-number") -#let __question-point = state("question-point", 0) -#let __question-point-position-state = state("question-point-position", left) - - -#let __localization = state("localization") - -#let __default-localization = ( - grade-table-queston: "Question", - grade-table-total: "Total", - grade-table-points: "Points", - grade-table-calification: "Calification", - point: "point", - points: "points", - page: "Page", - page-counter-display: "1 of 1", - family-name: "Surname", - personal-name: "Name", - group: "Group", - date: "Date" - ) - -#let __student-data(show-line-two: true) = { - locate(loc => { - [#__localization.final(loc).family-name: #box(width: 2fr, repeat[.]) #__localization.final(loc).personal-name: #box(width:1fr, repeat[.])] - if show-line-two { - v(1pt) - align(right, [#__localization.final(loc).group: #box(width:2.5cm, repeat[.]) #__localization.final(loc).date: #box(width:3cm, repeat[.])]) - } - } - ) -} - -#let __grade-table-header(decimal-separator: ".") = { - locate(loc => { - let end-question-locations = query(, loc) - let columns-number = range(0, end-question-locations.len() + 1) - - let question-row = columns-number.map(n => { - if n == 0 {align(left + horizon)[#text(hyphenate: false,__localization.final(loc).grade-table-queston)]} - else if n == end-question-locations.len() {align(left + horizon)[#text(hyphenate: false,__localization.final(loc).grade-table-total)]} - else [ #n ] - } - ) - - let total-point = 0 - if end-question-locations.len() > 0 { - total-point = end-question-locations.map(ql => __question-point.at(ql.location())).sum() - } - - let points = () - if end-question-locations.len() > 0 { - points = end-question-locations.map(ql => __question-point.at(ql.location())) - } - - let point-row = columns-number.map(n => { - if n == 0 {align(left + horizon)[#text(hyphenate: false,__localization.final(loc).grade-table-points)]} - else if n == end-question-locations.len() [ - #strfmt("{0:}", calc.round(total-point, digits:2), fmt-decimal-separator: decimal-separator) - ] - else { - let point = points.at(n) - [ - #strfmt("{0}", calc.round(point, digits: 2), fmt-decimal-separator: decimal-separator) - ] - } - } - ) - - let calification-row = columns-number.map(n => - { - if n == 0 { - align(left + horizon)[#text(hyphenate: false, __localization.final(loc).grade-table-calification)] - } - } - ) - - align(center, table( - stroke: 0.8pt + luma(80), - columns: columns-number.map( n => - { - if n == 0 {auto} - else if n == end-question-locations.len() {auto} - else {30pt} - }), - rows: (auto, auto, 30pt), - ..question-row.map(n => n), - ..point-row.map(n => n), - ..calification-row.map(n => n), - ) - ) - } - ) -} - -#let __question-numbering(..args) = { - let nums = args.pos() - if nums.len() == 1 { - numbering("1. ", nums.last()) - } - else if nums.len() == 2 { - numbering("(a) ", nums.last()) - } - else if nums.len() == 3 { - numbering("(i) ", nums.last()) - } -} - -#let __paint-tab(point: none, loc: none) = { - if point != none { - let label-point = __localization.final(loc).points - if point == 1 { - label-point = __localization.final(loc).point - } - - [(#emph[#strfmt("{0}", calc.round(point, digits: 2), fmt-decimal-separator: ",") #label-point])] - } -} - -#let g-question(point: none, body) = { - __question-number.step(level: 1) - - [#hide[]] - __question-point.update(p => - { - if point == none { 0 } - else { point } - }) - - locate(loc => { - let __question-point-position = __question-point-position-state.final(loc) - - if __question-point-position == left { - v(0.1em) - { - __question-number.display(__question-numbering) - if(point != none) { - __paint-tab(point:point, loc: loc) - h(0.2em) - } - } - body - } - else if __question-point-position == right { - v(0.1em) - if(point != none) { - place(right, - dx: 12%, - float: false, - __paint-tab(point: point, loc: loc)) - } - __question-number.display(__question-numbering) - body - } - else { - v(0.1em) - __question-number.display(__question-numbering) - body - } - }) -} - -#let g-subquestion(point: none, body) = { - __question-number.step(level: 2) - - let subquestion-point = 0 - if point != none { subquestion-point = point } - __question-point.update(p => p + subquestion-point ) - - locate(loc => { - let question-point-position = __question-point-position-state.final(loc) - - if question-point-position == left { - v(0.1em) - { - h(0.7em) - __question-number.display(__question-numbering) - if(point != none) { - __paint-tab(point: point, loc:loc) - h(0.2em) - } - } - body - } - else if question-point-position == right { - v(0.1em) - if(point != none) { - place(right, - dx: 12%, - float: false, - __paint-tab(point: point, loc:loc)) - } - { - h(0.7em) - __question-number.display(__question-numbering) - } - body - } - else { - v(0.1em) - { - h(0.7em) - __question-number.display(__question-numbering) - } - body - } - } - ) -} - -#let __show_clarifications = (clarifications: none) => { - if clarifications != none { - let clarifications-content = [] - if type(clarifications) == "content" { - clarifications-content = clarifications - } - else if type(clarifications) == "string" { - clarifications-content = clarifications - } - else if type(clarifications) == "array" { - clarifications-content = [ - #for clarification in clarifications [ - - #clarification - ] - ] - } - else { - panic("Not implementation clarificationso of type: '" + type(clarifications) + "'") - } - - rect( - width: 100%, - stroke: luma(120), - inset:8pt, - radius: 4pt, - clarifications-content - ) - - v(5pt) - } -} - -#let g-exam( - author: ( - name: "", - email: none, - watermark: none - ), - school: ( - name: none, - logo: none, - ), - exam-info: ( - academic-period: none, - academic-level: none, - academic-subject: none, - number: none, - content: none, - model: none - ), - localization: ( - grade-table-queston: none, - grade-table-total: none, - grade-table-points: none, - grade-table-calification: none, - point: none, - points: none, - page: none, - page-counter-display: none, - family-name: none, - personal-name: none, - group: none, - date: none - ), - // date: none auto datetime, - date: none, - keywords: none, - languaje: "en", - clarifications: none, - show-studen-data: "first-page", - show-grade-table: true, - decimal-separator: ".", - question-point-position: left, - body, -) = { - - assert(show-studen-data in (none, "first-page", "odd-pages"), - message: "Invalid show studen data") - - assert(question-point-position in (none, left, right), - message: "Invalid question point position") - - assert(decimal-separator in (".", ","), - message: "Invalid decimal separator") - - let __show-watermark = ( - author: ( - name: "", - email: none, - watermark: none - ), - school: ( - name: none, - logo: none, - ), - exam-info: ( - academic-period: none, - academic-level: none, - academic-subject: none, - number: none, - content: none, - model: none - ), - ) => { - place( - top + right, - float: true, - clearance: 0pt, - dx:72pt, - dy:-115pt, - rotate(270deg, - origin: top + right, - { - if author.at("watermark", default: none) != none { - text(size:7pt, fill:luma(90))[#author.watermark] - h(35pt) - } - if exam-info.at("model", default: none) != none { - text(size:8pt, luma(40))[#exam-info.model] - } - } - ) - ) - } - - let __document-name = ( - exam-info: ( - academic-period: none, - academic-level: none, - academic-subject: none, - number: none, - content: none, - model: none - )) => { - let document-name = "" - if exam-info.at("name", default: none) != none { document-name += " " + exam-info.name } - if exam-info.at("content", default: none) != none { document-name += " " + exam-info.content } - if exam-info.at("number", default: none) != none { document-name += " " + exam-info.number } - if exam-info.at("model", default: none) != none { document-name += " " + exam-info.model } - - return document-name - } - - let __read-localization = ( - languaje: "en", - localization: ( - grade-table-queston: none, - grade-table-total: none, - grade-table-points: none, - grade-table-calification: none, - point: none, - points: none, - page: none, - page-counter-display: none, - family-name: none, - personal-name: none, - group: none, - date: none - )) => { - let __lang_data = toml("lang.toml") - if(__lang_data != none) { - let __read_lang_data = __lang_data.at(languaje, default: localization) - - if(__read_lang_data != none) { - let __read-localization_value = (read_lang_data: none, field: "", localization: none) => { - let __parameter_value = localization.at(field) - if(__parameter_value != none) { return __parameter_value } - - let value = read_lang_data.at(field, default: __default-localization.at(field)) - if(value == none) { value = __default-localization.at(field)} - - return value - } - - let __grade_table_queston = __read-localization_value(read_lang_data: __read_lang_data, field: "grade-table-queston", localization: localization) - let __grade_table_total = __read-localization_value(read_lang_data: __read_lang_data, field: "grade-table-total", localization: localization) - let __grade_table_points = __read-localization_value(read_lang_data: __read_lang_data, field: "grade-table-points", localization: localization) - let __grade_table_calification = __read-localization_value(read_lang_data: __read_lang_data, field: "grade-table-calification", localization: localization) - let __point = __read-localization_value(read_lang_data: __read_lang_data, field:"point", localization: localization) - let __points = __read-localization_value(read_lang_data: __read_lang_data, field: "points", localization: localization) - let __page = __read-localization_value(read_lang_data: __read_lang_data, field: "page", localization: localization) - let __page-counter-display = __read-localization_value(read_lang_data: __read_lang_data, field: "page-counter-display", localization: localization) - let __family_name = __read-localization_value(read_lang_data: __read_lang_data, field: "family-name", localization: localization) - let __personal_name = __read-localization_value(read_lang_data: __read_lang_data, field: "personal-name", localization: localization) - let __group = __read-localization_value(read_lang_data: __read_lang_data, field: "group", localization: localization) - let __date = __read-localization_value(read_lang_data: __read_lang_data, field: "date", localization: localization) - - let __localization_lang_data = ( - grade-table-queston: __grade_table_queston, - grade-table-total: __grade_table_total, - grade-table-points: __grade_table_points, - grade-table-calification: __grade_table_calification, - point: __point, - points: __points, - page: __page, - page-counter-display: __page-counter-display, - family-name: __family_name, - personal-name: __personal_name, - group: __group, - date: __date, - ) - - __localization.update(__localization_lang_data) - } - } - } - - set document( - title: __document-name(exam-info: exam-info).trim(" "), - author: author.name - ) - - let margin-right = 2.5cm - if (question-point-position == right) { - margin-right = 3cm - } - - set page( - paper: "a4", - margin: (top: 5cm, right:margin-right), - numbering: "1 / 1", - number-align: right, - header-ascent: 20%, - header:locate(loc => { - let page-number = counter(page).at(loc).first() - if (page-number==1) { - align(right)[#box( - width:108%, - grid( - columns: (auto, auto), - gutter:0.7em, - align(left + top)[ - #if(school.at("logo", default : none) != none) { - if(type(school.logo) == "content") { - school.logo - } - else if(type(school.logo) == "bytes") { - image.decode(school.logo, height:2.5cm, fit:"contain") - } - else { - assert(type(school.logo) in (none, "content", "bytes") , message: "school.logo be of type content or bytes.") - } - } - ], - grid( - rows: (auto, auto, auto), - gutter:1em, - grid( - columns: (auto, 1fr, auto), - align(left + top)[ - #school.name \ - #exam-info.academic-period \ - #exam-info.academic-level - ], - align(center + top)[ - // #exam-info.number #exam-info.content \ - ], - align(right + top)[ - #exam-info.at("academic-subject", default: none) \ - #exam-info.number \ - #exam-info.content - ], - ), - line(length: 100%, stroke: 1pt + gray), - if show-studen-data in ("first-page", "odd-pages") { - __student-data() - } - ) - ) - )] - } - else if calc.rem-euclid(page-number, 2) == 1 { - grid( - columns: (auto, 1fr, auto), - gutter:0.3em, - align(left + top)[ - #school.name \ - #exam-info.academic-period \ - #exam-info.academic-level - ], - align(center + top)[ - // #exam-info.number #exam-info.content \ - ], - align(right + top)[ - #exam-info.at("academic-subject", default: none) \ - #exam-info.number \ - #exam-info.content - ] - ) - line(length: 100%, stroke: 1pt + gray) - if show-studen-data == "odd-pages" { - __student-data(show-line-two: false) - } - } - else { - grid( - columns: (auto, 1fr, auto), - gutter:0.3em, - align(left + top)[ - #school.name \ - #exam-info.academic-period \ - #exam-info.academic-level - ], - align(center + top)[ - // #exam-info.number #exam-info.content \ - ], - align(right + top)[ - #exam-info.at("academic-subject", default: none) \ - #exam-info.number \ - #exam-info.content \ - ] - ) - line(length: 100%, stroke: 1pt + gray) - } - } - ), - - footer: locate(loc => { - line(length: 100%, stroke: 1pt + gray) - align(right)[ - #__localization.final(loc).page - #counter(page).display(__localization.final(loc).page-counter-display, both: true, - ) - ] - // grid( - // columns: (1fr, 1fr, 1fr), - // align(left)[#school.name], - // align(center)[#exam-info.academic-period], - // align(right)[ - // Página - // #counter(page).display({ - // "1 de 1"}, - // both: true, - // ) - // ] - // ) - - __show-watermark(author: author, school: school, exam-info: exam-info) - } - ) - ) - - set par(justify: true) - set text(font: "New Computer Modern") - - __read-localization(languaje: languaje, localization: localization) - __question-point-position-state.update(u => question-point-position) - - set text(lang:languaje) - - if show-grade-table == true { - __grade-table-header( - decimal-separator: decimal-separator, - ) - v(10pt) - } - - // show heading.where(level: 1): it => { - // set block(above: 1.2em, below: 1em) - // set text(12pt, weight: "semibold") - // question(point: none)[#it.body] - // } - - // show heading.where(level: 2): it => { - // set text(12pt, weight: "regular") - // subquestion(point: none)[#it.body] - // } - - - set par(justify: true) - - if clarifications != none { - __show_clarifications(clarifications: clarifications) - } - - body - - [#hide[]] - [#hide[]] -} - -#let g-explanation(size:8pt, body) = { - text(size:size)[$(*)$ #body] -} \ No newline at end of file diff --git a/src/auxiliary.typ b/src/auxiliary.typ new file mode 100644 index 0000000..563254d --- /dev/null +++ b/src/auxiliary.typ @@ -0,0 +1,231 @@ +#import "./global.typ" : * + +#let __g-student-data(show-line-two: true) = { + locate(loc => { + [#__g-localization.final(loc).family-name: #box(width: 2fr, repeat[.]) #__g-localization.final(loc).given-name: #box(width:1fr, repeat[.])] + if show-line-two { + v(1pt) + align(right, [#__g-localization.final(loc).group: #box(width:2.5cm, repeat[.]) #__g-localization.final(loc).date: #box(width:3cm, repeat[.])]) + } + } + ) +} + +#let __g-grade-table-header(decimal-separator: ".") = { + locate(loc => { + let end-g-question-locations = query(, loc) + let columns-number = range(0, end-g-question-locations.len() + 1) + + let question-row = columns-number.map(n => { + if n == 0 {align(left + horizon)[#text(hyphenate: false,__g-localization.final(loc).grade-table-queston)]} + else if n == end-g-question-locations.len() {align(left + horizon)[#text(hyphenate: false,__g-localization.final(loc).grade-table-total)]} + else [ #n ] + } + ) + + let total-point = 0 + if end-g-question-locations.len() > 0 { + total-point = end-g-question-locations.map(ql => __g-question-point.at(ql.location())).sum() + } + + let points = () + if end-g-question-locations.len() > 0 { + points = end-g-question-locations.map(ql => __g-question-point.at(ql.location())) + } + + let point-row = columns-number.map(n => { + if n == 0 {align(left + horizon)[#text(hyphenate: false,__g-localization.final(loc).grade-table-points)]} + else if n == end-g-question-locations.len() [ + #strfmt("{0:}", calc.round(total-point, digits:2), fmt-decimal-separator: decimal-separator) + ] + else { + let point = points.at(n) + [ + #strfmt("{0}", calc.round(point, digits: 2), fmt-decimal-separator: decimal-separator) + ] + } + } + ) + + let calification-row = columns-number.map(n => + { + if n == 0 { + align(left + horizon)[#text(hyphenate: false, __g-localization.final(loc).grade-table-calification)] + } + } + ) + + align(center, table( + stroke: 0.8pt + luma(80), + columns: columns-number.map( n => + { + if n == 0 {auto} + else if n == end-g-question-locations.len() {auto} + else {30pt} + }), + rows: (auto, auto, 30pt), + ..question-row.map(n => n), + ..point-row.map(n => n), + ..calification-row.map(n => n), + ) + ) + } + ) +} + +#let __g-show_clarifications = (clarifications: none) => { + if clarifications != none { + let clarifications-content = [] + if type(clarifications) == "content" { + clarifications-content = clarifications + } + else if type(clarifications) == "string" { + clarifications-content = clarifications + } + else if type(clarifications) == "array" { + clarifications-content = [ + #for clarification in clarifications [ + - #clarification + ] + ] + } + else { + panic("Not implementation clarificationso of type: '" + type(clarifications) + "'") + } + + rect( + width: 100%, + stroke: luma(120), + inset:8pt, + radius: 4pt, + clarifications-content + ) + + v(5pt) + } +} + +#let __document-name = ( + exam-info: ( + academic-period: none, + academic-level: none, + academic-subject: none, + number: none, + content: none, + model: none + )) => { + let document-name = "" + if exam-info.at("name", default: none) != none { document-name += " " + exam-info.name } + if exam-info.at("content", default: none) != none { document-name += " " + exam-info.content } + if exam-info.at("number", default: none) != none { document-name += " " + exam-info.number } + if exam-info.at("model", default: none) != none { document-name += " " + exam-info.model } + + return document-name +} + +#let __read-localization = ( + languaje: "en", + localization: ( + grade-table-queston: none, + grade-table-total: none, + grade-table-points: none, + grade-table-calification: none, + point: none, + points: none, + page: none, + page-counter-display: none, + family-name: none, + given-name: none, + group: none, + date: none + )) => { + let __lang_data = toml("./lang.toml") + if(__lang_data != none) { + let __read_lang_data = __lang_data.at(languaje, default: localization) + + if(__read_lang_data != none) { + let __read-localization_value = (read_lang_data: none, field: "", localization: none) => { + let __parameter_value = localization.at(field) + if(__parameter_value != none) { return __parameter_value } + + let value = read_lang_data.at(field, default: __g-default-localization.at(field)) + if(value == none) { value = __g-default-localization.at(field)} + + return value + } + + let __grade_table_queston = __read-localization_value(read_lang_data: __read_lang_data, field: "grade-table-queston", localization: localization) + let __grade_table_total = __read-localization_value(read_lang_data: __read_lang_data, field: "grade-table-total", localization: localization) + let __grade_table_points = __read-localization_value(read_lang_data: __read_lang_data, field: "grade-table-points", localization: localization) + let __grade_table_calification = __read-localization_value(read_lang_data: __read_lang_data, field: "grade-table-calification", localization: localization) + let __point = __read-localization_value(read_lang_data: __read_lang_data, field:"point", localization: localization) + let __points = __read-localization_value(read_lang_data: __read_lang_data, field: "points", localization: localization) + let __page = __read-localization_value(read_lang_data: __read_lang_data, field: "page", localization: localization) + let __page-counter-display = __read-localization_value(read_lang_data: __read_lang_data, field: "page-counter-display", localization: localization) + let __family_name = __read-localization_value(read_lang_data: __read_lang_data, field: "family-name", localization: localization) + let __given_name = __read-localization_value(read_lang_data: __read_lang_data, field: "given-name", localization: localization) + let __group = __read-localization_value(read_lang_data: __read_lang_data, field: "group", localization: localization) + let __date = __read-localization_value(read_lang_data: __read_lang_data, field: "date", localization: localization) + + let __g-localization_lang_data = ( + grade-table-queston: __grade_table_queston, + grade-table-total: __grade_table_total, + grade-table-points: __grade_table_points, + grade-table-calification: __grade_table_calification, + point: __point, + points: __points, + page: __page, + page-counter-display: __page-counter-display, + family-name: __family_name, + given-name: __given_name, + group: __group, + date: __date, + ) + + __g-localization.update(__g-localization_lang_data) + } + } +} + +#let __show-watermark = ( + author: ( + name: "", + email: none, + watermark: none + ), + school: ( + name: none, + logo: none, + ), + exam-info: ( + academic-period: none, + academic-level: none, + academic-subject: none, + number: none, + content: none, + model: none + ), + question-point-position: left, + ) => { + let dx = if question-point-position == left { 58pt } else { 72pt } + place( + top + right, + float: true, + clearance: 0pt, + // dx:72pt, + dx:dx, + dy:-115pt, + rotate(270deg, + origin: top + right, + { + if author.at("watermark", default: none) != none { + text(size:7pt, fill:luma(90))[#author.watermark] + h(35pt) + } + if exam-info.at("model", default: none) != none { + text(size:8pt, luma(40))[#exam-info.model] + } + } + ) + ) +} \ No newline at end of file diff --git a/src/g-clarification.typ b/src/g-clarification.typ new file mode 100644 index 0000000..5858654 --- /dev/null +++ b/src/g-clarification.typ @@ -0,0 +1,8 @@ +#import"./global.typ": * + +/// Show a clarification. +/// - size(length): Size of clarification. +/// - body(string, content): Body of clarification. +#let g-clarification(size:8pt, body) = { + text(size:size)[$(*)$ #body] +} \ No newline at end of file diff --git a/src/g-exam.typ b/src/g-exam.typ new file mode 100644 index 0000000..97b9faf --- /dev/null +++ b/src/g-exam.typ @@ -0,0 +1,296 @@ +#import "@preview/oxifmt:0.2.0": strfmt +#import "./global.typ" : * +#import "./auxiliary.typ": * +#import "./g-question.typ": * +#import "./g-solution.typ": * +#import "./g-clarification.typ": * + +/// Template for creating an exam. +/// +/// - autor: Infomation of autor of exam. +/// - name (string, content): Name of author of exam. +/// - email (string): e-mail of author of exam. +/// - watermark (string): Watermark with information about the author of the document. +/// - scholl: Information of scholl. +/// - name (string, content): Name of the school or institution generating the exam. +/// - logo (none, content, bytes): Logo of the school or institution generating the exam. +/// - exam-info: Information of exam +/// - academic-period(none, content, str): academic period. +/// - academic-level(none, content, str): acadmic level. +/// - academic-subject(none, content, str): acadmic subname, +/// - number(none, content, str): Number of exam. +/// - content(none, content, str): Conten of exam. +/// - model(none, content, str): Model of exam. +/// - date (sting): Date of generate document. +/// - keywords (string): keywords of document. +/// - languaje (en, es, de, fr, pt, it): Languaje of docuemnt. English, Spanish, German, Portuguese and Italian are defined. +/// Ejemplo buy bonito: +/// - clarifications (string, content, array): Clarifications of exam. It will appear in a box on the first page. +/// - question-text-parameters: Parameter of text in question and subquestion. For example, it allows us to change the text size of the questions. +/// - show-studen-data(none, true, false, "first-page", "odd-pages"): It shows a box for the student to enter their details. It can appear on the first page or on all odd-numbered pages. +/// - show-grade-table: (bool): Show grade table. +/// - decimal-separator: (".", ","): Indicates the decimal separation character. +/// - question-point-position: (none, left, right): Position of question point. +/// - show-solution: (true, false): It shows the solutions to the questions. +#let g-exam( + author: ( + name: "", + email: none, + watermark: none + ), + school: ( + name: none, + logo: none, + ), + exam-info: ( + academic-period: none, + academic-level: none, + academic-subject: none, + number: none, + content: none, + model: none + ), + languaje: "en", + localization: ( + grade-table-queston: none, + grade-table-total: none, + grade-table-points: none, + grade-table-calification: none, + point: none, + points: none, + page: none, + page-counter-display: none, + family-name: none, + given-name: none, + group: none, + date: none + ), + date: none, + keywords: none, + clarifications: none, + question-text-parameters: none, + show-studen-data: "first-page", + show-grade-table: true, + decimal-separator: ".", + question-point-position: left, + show-solution: true, + body, +) = { + + assert(show-studen-data in (none, true, false, "first-page", "odd-pages"), + message: "Invalid show studen data") + + assert(question-point-position in (none, left, right), + message: "Invalid question point position") + + assert(decimal-separator in (".", ","), + message: "Invalid decimal separator") + + assert(show-solution in (true, false), + message: "Invalid show solution value") + + set document( + title: __document-name(exam-info: exam-info).trim(" "), + author: author.name + ) + + let margin-right = 2.5cm + if (question-point-position == right) { + margin-right = 3cm + } + + set page( + paper: "a4", + margin: (top: 5cm, right:margin-right), + numbering: "1 / 1", + number-align: right, + header-ascent: 20%, + header:locate(loc => { + let page-number = counter(page).at(loc).first() + if (page-number==1) { + align(right)[#box( + width:108%, + grid( + columns: (auto, auto), + gutter:0.7em, + align(left + top)[ + #if(school.at("logo", default : none) != none) { + set image(height:2.5cm, width: 2.7cm, fit:"contain") + if(type(school.logo) == "content") { + school.logo + } + else if(type(school.logo) == "bytes") { + image.decode(school.logo, height:2.5cm, fit:"contain") + } + else { + assert(type(school.logo) in (none, "content", "bytes") , message: "school.logo be of type content or bytes.") + } + } + ], + grid( + rows: (auto, auto, auto), + gutter:1em, + grid( + columns: (auto, 1fr, auto), + align(left + top)[ + #school.name \ + #exam-info.academic-period \ + #exam-info.academic-level + ], + align(center + top)[ + // #exam-info.number #exam-info.content \ + ], + align(right + top)[ + #exam-info.at("academic-subject", default: none) \ + #exam-info.number \ + #exam-info.content + ], + ), + line(length: 100%, stroke: 1pt + gray), + if show-studen-data in (true, "first-page", "odd-pages") { + __g-student-data() + } + ) + ) + )] + } + else if calc.rem-euclid(page-number, 2) == 1 { + grid( + columns: (auto, 1fr, auto), + gutter:0.3em, + align(left + top)[ + #school.name \ + #exam-info.academic-period \ + #exam-info.academic-level + ], + align(center + top)[ + // #exam-info.number #exam-info.content \ + ], + align(right + top)[ + #exam-info.at("academic-subject", default: none) \ + #exam-info.number \ + #exam-info.content + ] + ) + line(length: 100%, stroke: 1pt + gray) + if show-studen-data == "odd-pages" { + __g-student-data(show-line-two: false) + } + } + else { + grid( + columns: (auto, 1fr, auto), + gutter:0.3em, + align(left + top)[ + #school.name \ + #exam-info.academic-period \ + #exam-info.academic-level + ], + align(center + top)[ + // #exam-info.number #exam-info.content \ + ], + align(right + top)[ + #exam-info.at("academic-subject", default: none) \ + #exam-info.number \ + #exam-info.content \ + ] + ) + line(length: 100%, stroke: 1pt + gray) + } + } + ), + + footer: locate(loc => { + line(length: 100%, stroke: 1pt + gray) + align(right)[ + #__g-localization.final(loc).page + #counter(page).display(__g-localization.final(loc).page-counter-display, both: true, + ) + ] + // grid( + // columns: (1fr, 1fr, 1fr), + // align(left)[#school.name], + // align(center)[#exam-info.academic-period], + // align(right)[ + // Página + // #counter(page).display({ + // "1 de 1"}, + // both: true, + // ) + // ] + // ) + + __show-watermark(author: author, school: school, exam-info: exam-info, question-point-position:question-point-position) + } + ) + ) + + set par(justify: true) + set text(font: "New Computer Modern") + + __read-localization(languaje: languaje, localization: localization) + __g-question-point-position-state.update(u => question-point-position) + __g-question-text-parameters-state.update(question-text-parameters) + + set text(lang:languaje) + + if show-grade-table == true { + __g-grade-table-header( + decimal-separator: decimal-separator, + ) + v(10pt) + } + + __g-show-solution.update(show-solution) + + set par(justify: true) + + if clarifications != none { + __g-show_clarifications(clarifications: clarifications) + } + + show regex("=\?"): it => { + let (sugar) = it.text.split() + g-question[] + } + + show regex("=\? (.+)"): it => { + let (sugar, ..rest) = it.text.split() + g-question[#rest.join(" ")] + } + + show regex("=\? [[:digit:]] (.+)"): it => { + let (sugar, point, ..rest) = it.text.split() + g-question(point:float(point))[#rest.join(" ")] + } + + show regex("==\?"): it => { + let (sugar) = it.text.split() + g-subquestion[] + } + + show regex("==\? (.+)"): it => { + let (sugar, ..rest) = it.text.split() + g-subquestion[#rest.join(" ")] + } + + show regex("==\? [[:digit:]] (.+)"): it => { + let (sugar, point, ..rest) = it.text.split() + g-subquestion(point:float(point))[#rest.join(" ")] + } + + show regex("=! (.+)"): it => { + let (sugar, ..rest) = it.text.split() + g-solution[#rest.join(" ")] + } + + show regex("=% (.+)"): it => { + let (sugar, ..rest) = it.text.split() + g-clarification[#rest.join(" ")] + } + + body + + [#hide[]] + [#hide[]] +} diff --git a/src/g-question.typ b/src/g-question.typ new file mode 100644 index 0000000..43fdf72 --- /dev/null +++ b/src/g-question.typ @@ -0,0 +1,139 @@ +#import"./global.typ": * + +/// Show a question. +/// +/// *Example:* +/// ``` +/// #g-question(point:2)[This is a question] +/// ``` +/// +/// - point (none, float): Points of the question. +/// - point-position (none, left, right): Position of points. If none, use the position defined in G-Exam. +/// - body (string, content): Body of question. +#let g-question( + point: none, + point-position: none, + body) = { + assert(point-position in (none, left, right), + message: "Invalid point position") + + __g-question-number.step(level: 1) + + [#hide[]] + __g-question-point.update(p => + { + if point == none { 0 } + else { point } + }) + + locate(loc => { + let __g-question-point-position = point-position + if __g-question-point-position == none { + __g-question-point-position = __g-question-point-position-state.final(loc) + } + let __g-question-text-parameters = __g-question-text-parameters-state.final(loc) + + if __g-question-point-position == left { + v(0.1em) + { + __g-question-number.display(__g-question-numbering) + if(point != none) { + __g-paint-tab(point:point, loc: loc) + h(0.2em) + } + } + set text(..__g-question-text-parameters) + body + } + else if __g-question-point-position == right { + v(0.1em) + if(point != none) { + place(right, + dx: 15%, + float: false, + __g-paint-tab(point: point, loc: loc)) + } + __g-question-number.display(__g-question-numbering) + set text(..__g-question-text-parameters) + body + } + else { + v(0.1em) + __g-question-number.display(__g-question-numbering) + set text(..__g-question-text-parameters) + body + } + }) +} + +/// Show a sub-question. +/// +/// *Example:* +/// ``` +/// #g-subquestion(point:2)[This is a sub-question] +/// ``` +/// +/// - point (none, float): Points of the sub-question. +/// - point-position (none, left, right): Position of points. If none, use the position defined in G-Exam. +/// - body (string, content): Body of sub-question. +#let g-subquestion( + point: none, + point-position: none, + body) = { + + assert(point-position in (none, left, right), + message: "Invalid point position") + + __g-question-number.step(level: 2) + + let subg-question-point = 0 + if point != none { subg-question-point = point } + __g-question-point.update(p => p + subg-question-point ) + + locate(loc => { + let __g-question-point-position = point-position + if __g-question-point-position == none { + __g-question-point-position = __g-question-point-position-state.final(loc) + } + let __g-question-text-parameters = __g-question-text-parameters-state.final(loc) + + if __g-question-point-position == left { + v(0.1em) + { + h(0.7em) + __g-question-number.display(__g-question-numbering) + if(point != none) { + __g-paint-tab(point: point, loc:loc) + h(0.2em) + } + } + set text(..__g-question-text-parameters) + body + } + else if __g-question-point-position == right { + v(0.1em) + if(point != none) { + place(right, + dx: 15%, + float: false, + __g-paint-tab(point: point, loc:loc)) + } + { + h(0.7em) + __g-question-number.display(__g-question-numbering) + } + set text(..__g-question-text-parameters) + body + } + else { + v(0.1em) + { + h(0.7em) + __g-question-number.display(__g-question-numbering) + } + set text(..__g-question-text-parameters) + body + } + } + ) +} diff --git a/src/g-solution.typ b/src/g-solution.typ new file mode 100644 index 0000000..8b90168 --- /dev/null +++ b/src/g-solution.typ @@ -0,0 +1,33 @@ +#import"./global.typ": * + +/// Show solution of question. +/// +/// *Example:* +/// ``` #g-solution( +/// alternative-content: v(1fr) +/// )[ +/// I know the demostration, but there's no room on the margin. For any clarification ask Andrew Whilst. +/// ]``` +/// +/// +/// - alternative-content (string, content): Alternate content when the question solution is not displayed. +/// - body (string, content): Body of question solution +#let g-solution( + alternative-content: none, + body) = { + assert(alternative-content == none or type(alternative-content) == "content", + message: "Invalid alternative-content value") + + locate(loc => { + let show-solution = __g-show-solution.final(loc) + + if show-solution == true { + body + } + else { + hide[#body] + // alternative-content + } + } + ) +} \ No newline at end of file diff --git a/src/global.typ b/src/global.typ new file mode 100644 index 0000000..795e482 --- /dev/null +++ b/src/global.typ @@ -0,0 +1,48 @@ +#import "@preview/oxifmt:0.2.0": strfmt + +#let __g-question-number = counter("g-question-number") +#let __g-question-point = state("g-question-point", 0) +#let __g-question-point-position-state = state("g-question-point-position", left) +#let __g-question-text-parameters-state = state("question-text-parameters:", none) + +#let __g-localization = state("localization") +#let __g-show-solution = state("g-show-solution", false) + +#let __g-default-localization = ( + grade-table-queston: "Question", + grade-table-total: "Total", + grade-table-points: "Points", + grade-table-calification: "Calification", + point: "point", + points: "points", + page: "Page", + page-counter-display: "1 of 1", + family-name: "Surname", + given-name: "Name", + group: "Group", + date: "Date" + ) + +#let __g-question-numbering(..args) = { + let nums = args.pos() + if nums.len() == 1 { + numbering("1. ", nums.last()) + } + else if nums.len() == 2 { + numbering("(a) ", nums.last()) + } + else if nums.len() == 3 { + numbering("(i) ", nums.last()) + } +} + +#let __g-paint-tab(point: none, loc: none) = { + if point != none { + let label-point = __g-localization.final(loc).points + if point == 1 { + label-point = __g-localization.final(loc).point + } + + [(#emph[#strfmt("{0}", calc.round(point, digits: 2), fmt-decimal-separator: ",") #label-point])] + } +} \ No newline at end of file diff --git a/lang.toml b/src/lang.toml similarity index 92% rename from lang.toml rename to src/lang.toml index 2ce535a..40dc027 100644 --- a/lang.toml +++ b/src/lang.toml @@ -8,7 +8,7 @@ points = "points" page = "Page" page-counter-display = "1 of 1" family-name = "Surname" -personal-name = "Name" +given-name = "Name" group = "Group" date = "Date" @@ -22,7 +22,7 @@ points = "puntos" page = "Página" page-counter-display = "1 de 1" family-name = "Apellidos" -personal-name = "Nombre" +given-name = "Nombre" group = "Grupo" date = "Fecha" @@ -36,7 +36,7 @@ points = "points" page = "Seite" page-counter-display = "1 von 1" family-name = "Nachname" -personal-name = "Vor-und" +given-name = "Vor-und" group = "Gruppe" date = "Datum" @@ -50,7 +50,7 @@ points = "aiguillage" page = "Page" page-counter-display = "1 sur 1" family-name = "Prénom" -personal-name = "Nom" +given-name = "Nom" group = "Groupe" date = "Date" @@ -64,7 +64,7 @@ points = "pontos" page = "Página" page-counter-display = "1 de 1" family-name = "Apelido" -personal-name = "Nome" +given-name = "Nome" group = "Grupo" date = "Data" @@ -78,6 +78,6 @@ points = "punti" page = "Pagina" page-counter-display = "1 di 1" family-name = "Cognome" -personal-name = "Nome" +given-name = "Nome" group = "Gruppo" date = "Dattero" diff --git a/src/lib.typ b/src/lib.typ new file mode 100644 index 0000000..84d9d63 --- /dev/null +++ b/src/lib.typ @@ -0,0 +1,5 @@ +#let version = version((0,3,0)) + +#import "g-exam.typ": g-exam, g-question, g-subquestion, g-solution, g-clarification +// #import "g-command.typ": g-question, g-subquestion, g-solution, g-clarification +// #import "g-sugar.typ": * diff --git a/template/1er exam.typ b/template/1er exam.typ new file mode 100644 index 0000000..fe84251 --- /dev/null +++ b/template/1er exam.typ @@ -0,0 +1,36 @@ +#import "@preview/g-exam:0.2.0": g-exam, g-question, g-subquestion + +#show: g-exam.with( + school: ( + name: "My School", + logo: image("./logo.png") + ), + exam-info: ( + academic-period: "Academic year 2023/2024", + academic-level: "1st Secondary Education", + academic-subject: "Mathematics", + number: "2nd Assessment 1st Exam", + content: "Proofs", + model: "Model A" + ), + + languaje: "en", + decimal-separator: ",", + show-grade-table: true, + question-point-position: left, + clarifications: "Answer the questions in the spaces provided. If you run out of room for an answer, continue on the back of the page." +) + +#g-question[Given the equation $x^n + y^n = z^n$ for $(x,y,z)$ and $n$ positive integers.] +#g-subquestion(point:2)[For what values of $n$ is the statement in the previous question true?] +#v(1fr) +#g-subquestion(point:3)[For $n=2$ there's a theorem with a special name. What's that name?] +#v(1fr) + +#g-subquestion[What famous mathematician had an elegant proof for this theorem but +there was not enough space in the margin to write it down?]. +#v(1fr) + +#g-question(point:5)[Prove that the real part of all non-trivial zeros of the function $zeta(z) "is" 1/2$]. +#v(1fr) + diff --git a/template/logo.png b/template/logo.png new file mode 100644 index 0000000..385a501 Binary files /dev/null and b/template/logo.png differ diff --git a/typst.toml b/typst.toml deleted file mode 100644 index a832a4c..0000000 --- a/typst.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "g-exam" -version = "0.1.0" -entrypoint = "g-exam.typ" -authors = ["Andrés Giménez Muñoz"] -license = "MIT" -description = "Create exams with student information, grade chart, score control, questions, and sub-questions." -keywords = ["exam", "question", "subquestion"] -repository = "https://github.com/MatheSchool/typst-g-exam" -exclude = ["/examples/*"]