├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── coruscant_de.html ├── coruscant_de.pdf ├── coruscant_en.html ├── coruscant_en.pdf ├── kirk.jpg ├── kirk_resume_de.yaml └── kirk_resume_en.yaml ├── git-conventional-commits.json └── src ├── io ├── load_json_resume.rs ├── mod.rs ├── resolve_image_path.rs └── save_to_pdf.rs ├── main.rs └── templates ├── coruscant ├── basics │ ├── basics_box.rs │ ├── contact_info │ │ ├── contact_info_wrapper.rs │ │ ├── icons │ │ │ ├── address.svg │ │ │ ├── email.svg │ │ │ └── phone.svg │ │ ├── index.html │ │ └── mod.rs │ ├── index.html │ ├── languages │ │ ├── index.html │ │ ├── language_wrapper.rs │ │ └── mod.rs │ ├── mod.rs │ └── skills │ │ ├── index.html │ │ ├── mod.rs │ │ └── skills_wrapper.rs ├── data_model │ ├── basics.rs │ ├── education.rs │ ├── language.rs │ ├── location.rs │ ├── mod.rs │ ├── publication.rs │ ├── supported_resume_data.rs │ ├── utils.rs │ └── work.rs ├── education │ ├── education_wrapper.rs │ ├── index.html │ └── mod.rs ├── index.html ├── mod.rs ├── publication │ ├── index.html │ ├── mod.rs │ └── publication_wrapper.rs ├── shared │ ├── entry.rs │ ├── index.html │ ├── mod.rs │ └── render_template.rs ├── style.css ├── supported_languages.rs ├── template.rs └── work │ ├── index.html │ ├── mod.rs │ └── work_wrapper.rs ├── mod.rs └── template.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | #.idea/ 22 | 23 | # Added by cargo 24 | /target 25 | 26 | # MacOS 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | .DocumentRevisions-V100 31 | .fseventsd 32 | .Spotlight-V100 33 | .TemporaryItems 34 | .Trashes 35 | .VolumeIcon.icns 36 | .com.apple.timemachine.donotpresent 37 | 38 | # Visual Studio Code 39 | .vscode/* 40 | 41 | # Sandbox directory for all files that should stay private 42 | playground/ 43 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: [commit-msg, pre-commit] 2 | default_stages: [commit, merge-commit] 3 | minimum_pre_commit_version: 3.2.0 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.5.0 7 | hooks: 8 | - id: check-added-large-files 9 | - id: check-ast 10 | - id: check-builtin-literals 11 | - id: check-case-conflict 12 | - id: check-executables-have-shebangs 13 | - id: check-json 14 | - id: check-merge-conflict 15 | - id: check-shebang-scripts-are-executable 16 | - id: check-symlinks 17 | - id: check-toml 18 | - id: check-vcs-permalinks 19 | - id: check-xml 20 | - id: check-yaml 21 | - id: debug-statements 22 | - id: destroyed-symlinks 23 | 24 | - repo: https://github.com/doublify/pre-commit-rust 25 | rev: master 26 | hooks: 27 | - id: fmt 28 | name: cargo fmt 29 | - id: clippy 30 | name: cargo clippy 31 | 32 | - repo: local 33 | hooks: 34 | - id: cargo-test 35 | name: cargo test 36 | description: Run tests 37 | entry: cargo test 38 | language: system 39 | pass_filenames: false 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsume" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | clap = { version = "4.5.18", features = ["derive"] } 8 | headless_chrome = "1.0.15" 9 | json-resume = "0.2.0" 10 | minijinja = "2.3.1" 11 | serde = "1.0.210" 12 | serde_json = "1.0.128" 13 | serde_yaml = "0.9.34" 14 | tempfile = "3.12.0" 15 | 16 | [profile.release] 17 | lto = true 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rsume 2 | A tool for effortlessly generating resumes. 3 | 4 | ## Elevator Pitch 5 | The hiring process as a software developer can be super tedious. You never hear back from the majority of companies you have applied to and if they reach out, you have to jump through a lot of hoops to land the job. Since it is recommended to customize your resume for every single application, it may mean that you have to create dozens of resume before finally getting hired. Who has time for that? This tool is here to simplify the process by generating a high-quality resume with minimal work required. 6 | 7 | ## Getting started 8 | Currently, the only supported method for installing this program is by downloading or cloning this repo and building the binary yourself using `cargo` or `rustc`. 9 | 10 | An instance of Google Chrome or Chromedriver is required for executing the program. 11 | 12 | ## Usage 13 | `rsume`should be used from the command line like this: 14 | ```bash 15 | rsume /path/to/resume_data.yaml /target/path.pdf --template "coruscant" --language "english" 16 | ``` 17 | The `--template` and `--language` options are optional. 18 | 19 | The resume data should follow the [JSONResume](https://jsonresume.org/) schema and can either be stored as a `.json` or `.yaml` file. Look at [examples/kirk_resume_en.yaml](https://github.com/unexcellent/rsume/blob/main/examples/kirk_resume_en.yaml). 20 | 21 | ## Known Issues 22 | Currently, only a single template is available. In the future more template are planned. 23 | - If the content of the resume is short enough that only one page is filled, an empty second page is generated regardless 24 | - Page breaks in the coruscant template may separate the section title (like "Education") from the first entry -------------------------------------------------------------------------------- /examples/coruscant_de.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 297 | 298 | 299 |
300 |
301 |
302 |
303 |
304 | 305 |
306 |
307 |
308 | James T. Kirk 309 |
310 |
311 | Starfleet Captain 312 |
313 |
314 |
315 | 321 | 324 |
325 | 326 | 327 | 328 | 329 |
330 |
331 | 1-919-271-0076 332 |
333 |
334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 |
342 |
343 | 1234 Sunny Ave.,
344 | 52327 Riverside, IA 345 |
346 |
347 |
348 |
349 |
350 |
Sprachen
351 | 352 | 353 |
EN
354 |
355 |
356 |
357 |
358 | C2 359 |
360 | 361 |
DE
362 |
363 |
364 |
365 |
366 | B1 367 |
368 | 369 |
KL
370 |
371 |
372 |
373 |
374 | A2 375 |
376 | 377 |
378 |
379 |
Kenntnisse
380 | 381 |
Raumschiffe Kommandieren
382 | 383 |
Crewmitglieder Managen
384 | 385 |
Interplanetäre Kriegsführung
386 | 387 |
MS Excel
388 | 389 |
Schießen
390 | 391 |
Diplotie
392 | 393 |
394 |
395 |
396 |
397 |
398 | Arbeitserfahrung 399 |
400 |
401 |
402 |
403 |
404 | Fortlaufend 405 |
406 |
407 | 408 |
409 |
410 | Jul 2265 411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 | U.S.S Enterprise NCC-1701 421 |
422 |
423 | Kapitän 424 |
    425 | 426 |
  • Historische 5 jährige Mission
  • 427 | 428 | 429 |
  • Entdeckung von Methoden fürs Zeitreisen
  • 430 | 431 | 432 |
  • Auseinandersetzungen mit den Klingonen und Romulanern
  • 433 | 434 |
435 |
436 | 437 |
438 |
439 |
440 |
441 | Bildung 442 |
443 |
444 |
445 |
446 |
447 | Nov 2254 448 |
449 |
450 | 451 |
452 |
453 | Jan 2252 454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 | Starfleet Academie 464 |
465 |
466 | Meister in Raumschiffsbetrieb 467 |
468 | 469 |
470 |
471 |
472 | 473 |
474 | 475 | -------------------------------------------------------------------------------- /examples/coruscant_de.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unexcellent/rsume/209479bf95a129d20bf12e497b5d26fedf2e5695/examples/coruscant_de.pdf -------------------------------------------------------------------------------- /examples/coruscant_en.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 297 | 298 | 299 |
300 |
301 |
302 |
303 |
304 | 305 |
306 |
307 |
308 | James T. Kirk 309 |
310 |
311 | Starfleet Captain 312 |
313 |
314 |
315 | 321 | 324 |
325 | 326 | 327 | 328 | 329 |
330 |
331 | 1-919-271-0076 332 |
333 |
334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 |
342 |
343 | 1234 Sunny Ave.,
344 | 52327 Riverside, IA 345 |
346 |
347 |
348 |
349 |
350 |
Languages
351 | 352 | 353 |
EN
354 |
355 |
356 |
357 |
358 | C2 359 |
360 | 361 |
KL
362 |
363 |
364 |
365 |
366 | A2 367 |
368 | 369 |
370 |
371 |
Skills
372 | 373 |
Commanding Starfleet Ships
374 | 375 |
Managing Crew
376 | 377 |
Interplanetary Tactical Warfare
378 | 379 |
MS Excel
380 | 381 |
Shooting
382 | 383 |
Diplomacy
384 | 385 |
386 |
387 |
388 |
389 |
390 | Experience 391 |
392 |
393 |
394 |
395 |
396 | Present 397 |
398 |
399 | 400 |
401 |
402 | Jul 2265 403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 | U.S.S Enterprise NCC-1701 413 |
414 |
415 | Commanding officer 416 |
    417 | 418 |
  • Historic five year mission
  • 419 | 420 | 421 |
  • Discovery of replicable means of time travel
  • 422 | 423 | 424 |
  • Conflicts with the Klingon and Romulan Empires
  • 425 | 426 |
427 |
428 | 429 |
430 |
431 |
432 |
433 |
434 |
435 | Jun 2265 436 |
437 |
438 | 439 |
440 |
441 | Feb 2259 442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 | U.S.S Farragut NCC-1647 452 |
453 |
454 | Lieutenant Commander and first officer. 455 |
    456 |
457 |
458 | 459 |
460 |
461 |
462 |
463 |
464 |
465 | Jan 2259 466 |
467 |
468 | 469 |
470 |
471 | Dec 2254 472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 | U.S.S Farragut NCC-1647 482 |
483 |
484 | Junior Officer 485 |
    486 | 487 |
  • Commanding planetary survey parties
  • 488 | 489 | 490 |
  • Participation in Klingon war
  • 491 | 492 |
493 |
494 | 495 |
496 |
497 |
498 |
499 |
500 |
501 | Nov 2254 502 |
503 |
504 | 505 |
506 |
507 | Dec 2252 508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 | U.S.S Republic NCC-1371 518 |
519 |
520 | Cadet 521 |
    522 | 523 |
  • Nuclear incident handling
  • 524 | 525 | 526 |
  • Promotion priority adjustment
  • 527 | 528 |
529 |
530 | 531 |
532 |
533 |
534 |
535 | Education 536 |
537 |
538 |
539 |
540 |
541 | Nov 2254 542 |
543 |
544 | 545 |
546 |
547 | Jan 2252 548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 | Starfleet Academy 558 |
559 |
560 | Master in Starship Operations 561 |
562 | 563 | 566 | 567 |
568 |
569 |
570 |
571 | Publications 572 |
573 |
574 |
575 |
576 |
577 | 578 |
579 |
580 | Jul 2260 581 |
582 |
583 | 584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 | A Comparative Analysis of Starship Propulsion Systems 594 |
595 |
596 | This paper compares the efficiency, speed, and safety of various starship propulsion systems, such as warp drives and impulse engines. It also explores the potential for future advancements in propulsion technology. 597 |
598 | 599 | 602 | 603 |
604 |
605 |
606 |
607 | 608 | -------------------------------------------------------------------------------- /examples/coruscant_en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unexcellent/rsume/209479bf95a129d20bf12e497b5d26fedf2e5695/examples/coruscant_en.pdf -------------------------------------------------------------------------------- /examples/kirk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unexcellent/rsume/209479bf95a129d20bf12e497b5d26fedf2e5695/examples/kirk.jpg -------------------------------------------------------------------------------- /examples/kirk_resume_de.yaml: -------------------------------------------------------------------------------- 1 | basics: 2 | name: James T. Kirk 3 | label: Starfleet Captain 4 | image: kirk.jpg 5 | email: kirk@starfleet.int 6 | phone: 1-919-271-0076 7 | location: 8 | address: 1234 Sunny Ave. 9 | postalCode: 52327 10 | city: Riverside 11 | countryCode: IA 12 | 13 | languages: 14 | - language: EN 15 | fluency: C2 16 | - language: DE 17 | fluency: B1 18 | - language: KL 19 | fluency: A2 20 | 21 | skills: 22 | - name: Raumschiffe Kommandieren 23 | - name: Crewmitglieder Managen 24 | - name: Interplanetäre Kriegsführung 25 | - name: MS Excel 26 | - name: Schießen 27 | - name: Diplotie 28 | 29 | work: 30 | - name: U.S.S Enterprise NCC-1701 31 | description: Kapitän 32 | startDate: Jul 2265 33 | endDate: Fortlaufend 34 | highlights: 35 | - Historische 5 jährige Mission 36 | - Entdeckung von Methoden fürs Zeitreisen 37 | - Auseinandersetzungen mit den Klingonen und Romulanern 38 | 39 | education: 40 | - institution: Starfleet Academie 41 | startDate: Jan 2252 42 | endDate: Nov 2254 43 | studyType: Meister 44 | area: Raumschiffsbetrieb 45 | -------------------------------------------------------------------------------- /examples/kirk_resume_en.yaml: -------------------------------------------------------------------------------- 1 | basics: 2 | name: James T. Kirk 3 | label: Starfleet Captain 4 | image: kirk.jpg 5 | email: kirk@starfleet.int 6 | phone: 1-919-271-0076 7 | location: 8 | address: 1234 Sunny Ave. 9 | postalCode: 52327 10 | city: Riverside 11 | countryCode: IA 12 | 13 | languages: 14 | - language: EN 15 | fluency: C2 16 | - language: KL 17 | fluency: A2 18 | 19 | skills: 20 | - name: Commanding Starfleet Ships 21 | - name: Managing Crew 22 | - name: Interplanetary Tactical Warfare 23 | - name: MS Excel 24 | - name: Shooting 25 | - name: Diplomacy 26 | 27 | work: 28 | - name: U.S.S Enterprise NCC-1701 29 | description: Commanding officer 30 | startDate: Jul 2265 31 | endDate: Present 32 | highlights: 33 | - Historic five year mission 34 | - Discovery of replicable means of time travel 35 | - Conflicts with the Klingon and Romulan Empires 36 | 37 | - name: U.S.S Farragut NCC-1647 38 | description: Lieutenant Commander and first officer. 39 | startDate: Feb 2259 40 | endDate: Jun 2265 41 | 42 | - name: U.S.S Farragut NCC-1647 43 | description: Junior Officer 44 | startDate: Dec 2254 45 | endDate: Jan 2259 46 | highlights: 47 | - Commanding planetary survey parties 48 | - Participation in Klingon war 49 | 50 | - name: U.S.S Republic NCC-1371 51 | description: Cadet 52 | startDate: Dec 2252 53 | endDate: Nov 2254 54 | highlights: 55 | - Nuclear incident handling 56 | - Promotion priority adjustment 57 | 58 | education: 59 | - institution: Starfleet Academy 60 | startDate: Jan 2252 61 | endDate: Nov 2254 62 | studyType: "Master" 63 | area: "Starship Operations" 64 | score: 87/100 65 | 66 | publications: 67 | - name: A Comparative Analysis of Starship Propulsion Systems 68 | publisher: IESDGE 69 | releaseDate: Jul 2260 70 | url: iesdge.starfleet.int/papers/kirk-crisis-management 71 | summary: This paper compares the efficiency, speed, and safety of various starship propulsion systems, such as warp drives and impulse engines. It also explores the potential for future advancements in propulsion technology. 72 | -------------------------------------------------------------------------------- /git-conventional-commits.json: -------------------------------------------------------------------------------- 1 | { 2 | "convention" : { 3 | "commitTypes": [ 4 | "build", 5 | "chore", 6 | "ci", 7 | "docs", 8 | "feat", 9 | "fix", 10 | "merge", 11 | "perf", 12 | "refactor", 13 | "revert", 14 | "test", 15 | 16 | "lint", 17 | "style" 18 | ], 19 | "commitScopes": [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/io/load_json_resume.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fs, path::PathBuf}; 2 | 3 | /// Try to load the contents of a JSONResume file into a struct. The file can either be of type .json or .yaml. 4 | pub fn load_json_resume(path: &PathBuf) -> Result> { 5 | let contents = fs::read_to_string(path)?; 6 | let resume_data = match path.extension().unwrap().to_str() { 7 | Some("json") => serde_json::from_str(&contents)?, 8 | Some("yaml") | Some("yml") => serde_yaml::from_str(&contents)?, 9 | _ => Err(format!( 10 | "Unsupported file extension: {:?}", 11 | path.extension() 12 | ))?, 13 | }; 14 | 15 | Ok(resume_data) 16 | } 17 | 18 | #[cfg(test)] 19 | pub mod tests { 20 | use super::*; 21 | use tempfile::TempDir; 22 | 23 | #[test] 24 | fn json() { 25 | let contents = " 26 | { 27 | \"basics\": { 28 | \"name\": \"Kirk\" 29 | } 30 | } 31 | "; 32 | 33 | let tmp_dir = TempDir::new().unwrap(); 34 | let file_path = tmp_dir.path().join("resume.json"); 35 | fs::write(&file_path, contents).unwrap(); 36 | 37 | let resume_data = load_json_resume(&file_path).unwrap(); 38 | assert_eq!(resume_data.basics.unwrap().name, Some("Kirk".to_string())); 39 | } 40 | 41 | #[test] 42 | fn yaml() { 43 | let contents = " 44 | basics: 45 | name: Kirk 46 | "; 47 | 48 | let tmp_dir = TempDir::new().unwrap(); 49 | let file_path = tmp_dir.path().join("resume.yaml"); 50 | fs::write(&file_path, contents).unwrap(); 51 | 52 | let resume_data = load_json_resume(&file_path).unwrap(); 53 | assert_eq!(resume_data.basics.unwrap().name, Some("Kirk".to_string())); 54 | } 55 | 56 | #[test] 57 | fn unsupported_type() { 58 | let contents = ""; 59 | 60 | let tmp_dir = TempDir::new().unwrap(); 61 | let file_path = tmp_dir.path().join("resume.UNSUPPORTED"); 62 | fs::write(&file_path, contents).unwrap(); 63 | 64 | let resume_data = load_json_resume(&file_path); 65 | assert!(resume_data.is_err()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod load_json_resume; 2 | pub mod resolve_image_path; 3 | pub mod save_to_pdf; 4 | -------------------------------------------------------------------------------- /src/io/resolve_image_path.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::Path}; 2 | 3 | /// Image paths can either be given as absolute or relative paths from the JSONResume file. This function resolves relative paths. 4 | pub fn resolve_image_path(resume_data_path: &Path, image_path: &Option) -> Option { 5 | image_path.as_ref().map(|img_path| { 6 | fs::canonicalize(resume_data_path.parent().unwrap().join(img_path)) 7 | .unwrap() 8 | .to_str() 9 | .unwrap() 10 | .to_string() 11 | }) 12 | } 13 | 14 | #[cfg(test)] 15 | pub mod tests { 16 | use tempfile::TempDir; 17 | 18 | use super::*; 19 | use std::path::PathBuf; 20 | 21 | #[test] 22 | fn none() { 23 | let resume_data_path = PathBuf::from("/path/to/some/resume.json"); 24 | let image_path = None; 25 | 26 | let actual = resolve_image_path(&resume_data_path, &image_path); 27 | assert_eq!(actual, None) 28 | } 29 | 30 | #[test] 31 | fn same_folder() { 32 | let tmp_dir = TempDir::new().unwrap(); 33 | let resume_data_path = tmp_dir.path().join("resume.json"); 34 | let image_path = Some("profile.png".to_string()); 35 | 36 | fs::write(&resume_data_path, "").unwrap(); 37 | fs::write(tmp_dir.path().join("profile.png"), "").unwrap(); 38 | 39 | let actual = resolve_image_path(&resume_data_path, &image_path); 40 | assert!(PathBuf::from(actual.unwrap()).is_file()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/io/save_to_pdf.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fs, path::PathBuf}; 2 | 3 | use headless_chrome::{types::PrintToPdfOptions, Browser}; 4 | use tempfile::TempDir; 5 | 6 | /// Save the resume design into a PDF. 7 | pub fn save_to_pdf(html_resume: String, target_path: &PathBuf) -> Result<(), Box> { 8 | let tmp_dir = TempDir::new()?; 9 | let tmp_storage_path = tmp_dir.path().join("resume.html"); 10 | fs::write(&tmp_storage_path, html_resume)?; 11 | 12 | let browser = Browser::default()?; 13 | let tab = browser.new_tab()?; 14 | let pdf = tab 15 | .navigate_to(&format!("file://{}", tmp_storage_path.to_str().unwrap()))? 16 | .wait_until_navigated()? 17 | .print_to_pdf(Some(PrintToPdfOptions { 18 | margin_bottom: Some(0.0), 19 | margin_top: Some(0.0), 20 | margin_left: Some(0.0), 21 | margin_right: Some(0.0), 22 | print_background: Some(true), 23 | ..PrintToPdfOptions::default() 24 | }))?; 25 | 26 | fs::write(target_path, pdf)?; 27 | 28 | Ok(()) 29 | } 30 | 31 | #[cfg(test)] 32 | pub mod tests { 33 | 34 | use super::*; 35 | 36 | #[test] 37 | fn file_is_written() { 38 | let html_resume = " 39 | 40 | 41 | 42 | 43 | "; 44 | 45 | let tmp_dir = TempDir::new().unwrap(); 46 | let target_path = tmp_dir.path().join("resume.pdf"); 47 | 48 | let result = save_to_pdf(html_resume.to_string(), &target_path); 49 | assert!(result.is_ok()); 50 | assert!(target_path.is_file()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod io; 2 | mod templates; 3 | 4 | use std::error::Error; 5 | use std::path::PathBuf; 6 | 7 | use crate::io::load_json_resume::load_json_resume; 8 | use crate::io::resolve_image_path::resolve_image_path; 9 | use crate::io::save_to_pdf::save_to_pdf; 10 | use crate::templates::Coruscant; 11 | use clap::Parser; 12 | use templates::template::Template; 13 | 14 | /// Program for generating a resume from JSONResume data. 15 | #[derive(Parser, Debug)] 16 | #[command(version, about, long_about = None)] 17 | pub struct Args { 18 | /// Path to the data describing your resume. It needs to comply with theJSONResume schema (see https://jsonresume.org/). 19 | resume_data_path: PathBuf, 20 | 21 | /// Location where the generated PDF should be stored. 22 | target_path: PathBuf, 23 | 24 | /// Template that should be used. Currently, the only available option is 'coruscant'. Default is 'coruscant'. 25 | #[arg(short, long)] 26 | template: Option, 27 | 28 | /// Language of the template. Available options are 'english' and 'deutsch'. Default is english. 29 | #[arg(short, long)] 30 | language: Option, 31 | } 32 | 33 | fn main() -> Result<(), Box> { 34 | let args = Args::parse(); 35 | 36 | let language = match args.language { 37 | Some(language_string) => GloballySupportedLanguages::try_from(language_string)?, 38 | None => GloballySupportedLanguages::EN, 39 | }; 40 | 41 | let template = match args.template { 42 | Some(template_string) => AvailableTemplates::try_from(template_string)?, 43 | None => AvailableTemplates::Coruscant, 44 | }; 45 | 46 | generate_pdf(args.resume_data_path, args.target_path, template, language)?; 47 | 48 | Ok(()) 49 | } 50 | 51 | /// Generate a resume and save it as a PDF. 52 | pub fn generate_pdf( 53 | resume_data_path: PathBuf, 54 | target_path: PathBuf, 55 | template_enum: AvailableTemplates, 56 | language: GloballySupportedLanguages, 57 | ) -> Result<(), Box> { 58 | let mut resume_data = load_json_resume(&resume_data_path).unwrap(); 59 | 60 | if let Some(ref mut basics) = resume_data.basics { 61 | basics.image = resolve_image_path(&resume_data_path, &basics.image); 62 | } 63 | 64 | let template = match template_enum { 65 | AvailableTemplates::Coruscant => Coruscant::new(resume_data, &language).unwrap(), 66 | }; 67 | 68 | let html_resume = template.build(); 69 | save_to_pdf(html_resume, &target_path)?; 70 | 71 | Ok(()) 72 | } 73 | 74 | /// All templates that are available. 75 | pub enum AvailableTemplates { 76 | /// A modern, minimalist, and professional resume design. 77 | Coruscant, 78 | } 79 | impl AvailableTemplates { 80 | /// Try constructing a this struct from a string. 81 | pub fn try_from(template_string: String) -> Result { 82 | match template_string.to_lowercase().as_str() { 83 | "coruscant" => Ok(AvailableTemplates::Coruscant), 84 | _ => Err(format!("{template_string} is not a supported template.")), 85 | } 86 | } 87 | } 88 | 89 | /// Language in which the resume should be generated in. 90 | pub enum GloballySupportedLanguages { 91 | /// English 92 | EN, 93 | /// German 94 | DE, 95 | } 96 | impl GloballySupportedLanguages { 97 | /// Try constructing a this struct from a string. 98 | pub fn try_from(language_string: String) -> Result { 99 | match language_string.to_lowercase().as_str() { 100 | "english" | "en" => Ok(GloballySupportedLanguages::EN), 101 | "deutsch" | "german" | "de" => Ok(GloballySupportedLanguages::DE), 102 | _ => Err(format!("{language_string} is not a supported language.")), 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/basics_box.rs: -------------------------------------------------------------------------------- 1 | use minijinja::context; 2 | 3 | use crate::templates::coruscant::{ 4 | basics::{ 5 | contact_info::contact_info_wrapper::build_contact_info_wrapper, 6 | languages::language_wrapper::build_languages_wrapper, 7 | skills::skills_wrapper::build_skills_wrapper, 8 | }, 9 | data_model::supported_resume_data::SupportedResumeData, 10 | shared::render_template::render_template, 11 | supported_languages::SupportedLanguages, 12 | }; 13 | 14 | /// Return the basics wrapper as HTML. 15 | pub fn build_basics_wrapper( 16 | resume_data: &SupportedResumeData, 17 | language: &SupportedLanguages, 18 | ) -> String { 19 | let rendered_template = render_template( 20 | include_str!("index.html"), 21 | context!( 22 | name => resume_data.basics.name, 23 | image => resume_data.basics.image, 24 | label => resume_data.basics.label, 25 | contact_info => build_contact_info_wrapper(resume_data), 26 | languages => build_languages_wrapper(resume_data, language), 27 | skills => build_skills_wrapper(resume_data, language), 28 | ), 29 | ); 30 | 31 | match rendered_template { 32 | Ok(t) => t, 33 | Err(_) => panic!("Failed to render basics template."), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/contact_info/contact_info_wrapper.rs: -------------------------------------------------------------------------------- 1 | use minijinja::context; 2 | 3 | use crate::templates::coruscant::{ 4 | data_model::supported_resume_data::SupportedResumeData, 5 | shared::render_template::render_template, 6 | }; 7 | 8 | /// Return the contact info wrapper as HTML. 9 | pub fn build_contact_info_wrapper(resume_data: &SupportedResumeData) -> String { 10 | let rendered_template = render_template( 11 | include_str!("index.html"), 12 | context!( 13 | email => resume_data.basics.email, 14 | phone => resume_data.basics.phone, 15 | address => resume_data.basics.location.address, 16 | city => resume_data.basics.location.city, 17 | postal_code => resume_data.basics.location.postal_code, 18 | country_code => resume_data.basics.location.country_code, 19 | email_icon => include_str!("icons/email.svg"), 20 | phone_icon => include_str!("icons/phone.svg"), 21 | address_icon => include_str!("icons/address.svg"), 22 | ), 23 | ); 24 | 25 | match rendered_template { 26 | Ok(t) => t, 27 | Err(_) => panic!("Failed to render contact info template."), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/contact_info/icons/address.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/contact_info/icons/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/contact_info/icons/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/contact_info/index.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 |
9 | {{phone_icon}} 10 |
11 |
12 | {{phone}} 13 |
14 |
15 | {{address_icon}} 16 |
17 |
18 | {{address}},
19 | {{postal_code}} {{city}}, {{country_code}} 20 |
21 |
-------------------------------------------------------------------------------- /src/templates/coruscant/basics/contact_info/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod contact_info_wrapper; 2 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 | {{name}} 10 |
11 |
12 | {{label}} 13 |
14 |
15 | {{contact_info}} 16 |
17 |
18 | {{languages}} 19 | {{skills}} 20 |
21 |
22 |
-------------------------------------------------------------------------------- /src/templates/coruscant/basics/languages/index.html: -------------------------------------------------------------------------------- 1 |
2 |
{{title}}
3 | 4 | {% for language in languages %} 5 |
{{language.language}}
6 |
7 |
8 |
9 |
10 | {{language.fluency}} 11 |
12 | {% endfor %} 13 |
-------------------------------------------------------------------------------- /src/templates/coruscant/basics/languages/language_wrapper.rs: -------------------------------------------------------------------------------- 1 | use minijinja::context; 2 | 3 | use crate::templates::coruscant::{ 4 | data_model::supported_resume_data::SupportedResumeData, 5 | shared::render_template::render_template, supported_languages::SupportedLanguages, 6 | }; 7 | 8 | /// Return the languages wrapper as HTML. 9 | pub fn build_languages_wrapper( 10 | resume_data: &SupportedResumeData, 11 | language: &SupportedLanguages, 12 | ) -> String { 13 | if resume_data.languages.is_empty() { 14 | return String::new(); 15 | } 16 | 17 | let percentages: Vec = resume_data 18 | .languages 19 | .iter() 20 | .map(|l| l.percentage().unwrap()) 21 | .collect(); 22 | 23 | let rendered_template = render_template( 24 | include_str!("index.html"), 25 | context!( 26 | title => language.languages_section_title(), 27 | languages => resume_data.languages, 28 | percentages => percentages, 29 | ), 30 | ); 31 | 32 | match rendered_template { 33 | Ok(t) => t, 34 | Err(_) => panic!("Failed to render languages template."), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/languages/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod language_wrapper; 2 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod basics_box; 2 | mod contact_info; 3 | mod languages; 4 | mod skills; 5 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/skills/index.html: -------------------------------------------------------------------------------- 1 |
2 |
{{title}}
3 | {% for skill in skills %} 4 |
{{skill}}
5 | {% endfor %} 6 |
-------------------------------------------------------------------------------- /src/templates/coruscant/basics/skills/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod skills_wrapper; 2 | -------------------------------------------------------------------------------- /src/templates/coruscant/basics/skills/skills_wrapper.rs: -------------------------------------------------------------------------------- 1 | use minijinja::context; 2 | 3 | use crate::templates::coruscant::{ 4 | data_model::supported_resume_data::SupportedResumeData, 5 | shared::render_template::render_template, supported_languages::SupportedLanguages, 6 | }; 7 | 8 | /// Return the skills wrapper as HTML. 9 | pub fn build_skills_wrapper( 10 | resume_data: &SupportedResumeData, 11 | language: &SupportedLanguages, 12 | ) -> String { 13 | if resume_data.skills.is_empty() { 14 | return String::new(); 15 | } 16 | 17 | let rendered_template = render_template( 18 | include_str!("index.html"), 19 | context!(title => language.skills_section_title(), skills => resume_data.skills), 20 | ); 21 | 22 | match rendered_template { 23 | Ok(t) => t, 24 | Err(_) => panic!("Failed to render skills template."), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/basics.rs: -------------------------------------------------------------------------------- 1 | use super::{location::Location, utils::get_mandatory_field}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Basics { 5 | pub name: String, 6 | pub label: String, 7 | pub image: String, 8 | pub email: String, 9 | pub phone: String, 10 | pub location: Location, 11 | } 12 | impl Basics { 13 | pub fn try_from(basics: json_resume::Basics) -> Result { 14 | Ok(Self { 15 | name: get_mandatory_field(basics.name, "basics.name")?, 16 | label: get_mandatory_field(basics.label, "basics.label")?, 17 | image: get_mandatory_field(basics.image, "basics.image")?, 18 | email: get_mandatory_field(basics.email, "basics.email")?, 19 | phone: get_mandatory_field(basics.phone, "basics.phone")?, 20 | location: Location::try_from(basics.location)?, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/education.rs: -------------------------------------------------------------------------------- 1 | use super::utils::get_mandatory_field; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Education { 5 | pub institution: String, 6 | pub start_date: String, 7 | pub end_date: String, 8 | pub study_type: Option, 9 | pub area: String, 10 | pub score: Option, 11 | } 12 | impl Education { 13 | pub fn try_from(education: json_resume::Education) -> Result { 14 | Ok(Self { 15 | institution: get_mandatory_field(education.institution, "education.institution")?, 16 | start_date: get_mandatory_field(education.start_date, "education.start_date")?, 17 | end_date: get_mandatory_field(education.end_date, "education.end_date")?, 18 | study_type: education.study_type, 19 | area: get_mandatory_field(education.area, "education.area")?, 20 | score: education.score, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/language.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use super::utils::get_mandatory_field; 4 | 5 | #[allow(dead_code)] 6 | #[derive(Clone, Debug, Serialize)] 7 | pub struct Language { 8 | pub language: String, 9 | pub fluency: String, 10 | } 11 | impl Language { 12 | pub fn try_from(language: json_resume::Language) -> Result { 13 | Ok(Self { 14 | language: get_mandatory_field(language.language, "language.language")?, 15 | fluency: get_mandatory_field(language.fluency, "language.fluency")?, 16 | }) 17 | } 18 | 19 | pub fn percentage(&self) -> Result { 20 | if self.fluency.ends_with("%") { 21 | match remove_last_char(&self.fluency).parse() { 22 | Ok(p) => return Ok(p), 23 | Err(_) => return Err(()), 24 | } 25 | } 26 | 27 | match self.fluency.as_str() { 28 | "A1" => Ok(17), 29 | "A2" => Ok(33), 30 | "B1" => Ok(50), 31 | "B2" => Ok(67), 32 | "C1" => Ok(83), 33 | "C2" => Ok(100), 34 | _ => Err(()), 35 | } 36 | } 37 | } 38 | 39 | fn remove_last_char(string: &str) -> &str { 40 | match string.char_indices().next_back() { 41 | Some((i, _)) => &string[..i], 42 | None => string, 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::*; 49 | 50 | #[test] 51 | fn percentage_from_number() { 52 | let language = Language { 53 | language: "".to_string(), 54 | fluency: "70%".to_string(), 55 | }; 56 | 57 | let actual = language.percentage(); 58 | assert_eq!(actual, Ok(70)); 59 | } 60 | 61 | #[test] 62 | fn percentage_from_level() { 63 | let language = Language { 64 | language: "".to_string(), 65 | fluency: "B2".to_string(), 66 | }; 67 | 68 | let actual = language.percentage(); 69 | assert_eq!(actual, Ok(67)); 70 | } 71 | 72 | #[test] 73 | fn percentage_unknown() { 74 | let language = Language { 75 | language: "".to_string(), 76 | fluency: "UNKNOWN".to_string(), 77 | }; 78 | 79 | let actual = language.percentage(); 80 | assert_eq!(actual, Err(())); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/location.rs: -------------------------------------------------------------------------------- 1 | use super::utils::get_mandatory_field; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Location { 5 | pub address: String, 6 | pub postal_code: String, 7 | pub city: String, 8 | pub country_code: String, 9 | } 10 | impl Location { 11 | pub fn try_from(location: json_resume::Location) -> Result { 12 | Ok(Self { 13 | address: get_mandatory_field(location.address, "location.address")?, 14 | postal_code: get_mandatory_field(location.postal_code, "location.postal_code")?, 15 | city: get_mandatory_field(location.city, "location.city")?, 16 | country_code: get_mandatory_field(location.country_code, "location.country_code")?, 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod basics; 2 | pub mod education; 3 | pub mod language; 4 | pub mod location; 5 | pub mod publication; 6 | pub mod supported_resume_data; 7 | mod utils; 8 | pub mod work; 9 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/publication.rs: -------------------------------------------------------------------------------- 1 | use super::utils::get_mandatory_field; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Publication { 5 | pub name: String, 6 | pub publisher: String, 7 | pub release_date: String, 8 | pub url: String, 9 | pub summary: String, 10 | } 11 | impl Publication { 12 | pub fn try_from(publication: json_resume::Publication) -> Result { 13 | Ok(Self { 14 | name: get_mandatory_field(publication.name, "publication.name")?, 15 | publisher: get_mandatory_field(publication.publisher, "publication.publisher")?, 16 | release_date: get_mandatory_field( 17 | publication.release_date, 18 | "publication.release_date", 19 | )?, 20 | url: get_mandatory_field(publication.url, "publication.url")?, 21 | summary: get_mandatory_field(publication.summary, "publication.summary")?, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/supported_resume_data.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | basics::Basics, education::Education, language::Language, publication::Publication, 3 | utils::get_mandatory_field, work::Work, 4 | }; 5 | 6 | /// The template requires some fields in the resume data that are optional in json_resume. These structs simplify the generation process. 7 | #[derive(Clone, Debug)] 8 | pub struct SupportedResumeData { 9 | pub basics: Basics, 10 | pub languages: Vec, 11 | pub skills: Vec, 12 | pub work: Vec, 13 | pub education: Vec, 14 | pub publications: Vec, 15 | } 16 | impl SupportedResumeData { 17 | pub fn try_from(resume_data: json_resume::Resume) -> Result { 18 | Ok(Self { 19 | basics: match resume_data.basics { 20 | Some(b) => Basics::try_from(b)?, 21 | None => return Err("The field basics is required for this template.".to_string()), 22 | }, 23 | languages: resume_data 24 | .languages 25 | .into_iter() 26 | .map(|l| Language::try_from(l).unwrap()) 27 | .collect(), 28 | skills: resume_data 29 | .skills 30 | .into_iter() 31 | .map(|skill| get_mandatory_field(skill.name, "skill.name").unwrap()) 32 | .collect(), 33 | work: resume_data 34 | .work 35 | .into_iter() 36 | .map(|w| Work::try_from(w).unwrap()) 37 | .collect(), 38 | education: resume_data 39 | .education 40 | .into_iter() 41 | .map(|edu| Education::try_from(edu).unwrap()) 42 | .collect(), 43 | publications: resume_data 44 | .publications 45 | .into_iter() 46 | .map(|publication| Publication::try_from(publication).unwrap()) 47 | .collect(), 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/utils.rs: -------------------------------------------------------------------------------- 1 | /// Return the content of an optional field if it is set or return an Err if it is not. 2 | pub fn get_mandatory_field(field: Option, field_name: &str) -> Result { 3 | match field { 4 | Some(f) => Ok(f), 5 | None => Err(format!( 6 | "The field {field_name} is required for this template." 7 | )), 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/coruscant/data_model/work.rs: -------------------------------------------------------------------------------- 1 | use super::utils::get_mandatory_field; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Work { 5 | pub name: String, 6 | pub description: String, 7 | pub start_date: String, 8 | pub end_date: String, 9 | pub highlights: Vec, 10 | } 11 | impl Work { 12 | pub fn try_from(work: json_resume::Work) -> Result { 13 | Ok(Self { 14 | name: get_mandatory_field(work.name, "education.institution")?, 15 | description: get_mandatory_field(work.description, "education.institution")?, 16 | start_date: get_mandatory_field(work.start_date, "education.start_date")?, 17 | end_date: get_mandatory_field(work.end_date, "education.end_date")?, 18 | highlights: work.highlights.iter().map(|h| h.to_string()).collect(), 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/templates/coruscant/education/education_wrapper.rs: -------------------------------------------------------------------------------- 1 | use minijinja::context; 2 | 3 | use crate::templates::coruscant::{ 4 | data_model::{education::Education, supported_resume_data::SupportedResumeData}, 5 | shared::{entry::build_entry_start_and_end, render_template::render_template}, 6 | supported_languages::SupportedLanguages, 7 | }; 8 | 9 | /// Return the education wrapper as HTML. 10 | pub fn build_education_wrapper( 11 | resume_data: &SupportedResumeData, 12 | language: &SupportedLanguages, 13 | ) -> String { 14 | if resume_data.education.is_empty() { 15 | return String::new(); 16 | } 17 | 18 | let rendered_template = render_template( 19 | include_str!("index.html"), 20 | context!(entries => build_entries(&resume_data.education), title => language.education_section_title()), 21 | ); 22 | 23 | match rendered_template { 24 | Ok(t) => t, 25 | Err(_) => panic!("Failed to render work template."), 26 | } 27 | } 28 | 29 | fn build_entries(education: &Vec) -> String { 30 | let mut entries_html = String::new(); 31 | 32 | for education_entry in education { 33 | entries_html.push_str(&build_entry_start_and_end( 34 | education_entry.start_date.clone(), 35 | education_entry.end_date.clone(), 36 | education_entry.institution.clone(), 37 | build_entry_body(education_entry), 38 | build_entry_footer(education_entry), 39 | )); 40 | } 41 | 42 | entries_html 43 | } 44 | 45 | fn build_entry_body(education_entry: &Education) -> String { 46 | let study_type = match education_entry.clone().study_type { 47 | None => "".to_string(), 48 | Some(s) => format!("{s} in "), 49 | }; 50 | let area = education_entry.clone().area; 51 | 52 | format!("{study_type}{area}") 53 | } 54 | 55 | fn build_entry_footer(education_entry: &Education) -> Option { 56 | education_entry 57 | .score 58 | .as_ref() 59 | .map(|score| format!("Score: {}", score)) 60 | } 61 | -------------------------------------------------------------------------------- /src/templates/coruscant/education/index.html: -------------------------------------------------------------------------------- 1 |
2 | {{title}} 3 |
4 | {{entries}} -------------------------------------------------------------------------------- /src/templates/coruscant/education/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod education_wrapper; 2 | -------------------------------------------------------------------------------- /src/templates/coruscant/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | {{basics}} 8 | {{work}} 9 | {{education}} 10 | {{publications}} 11 |
12 | 13 | -------------------------------------------------------------------------------- /src/templates/coruscant/mod.rs: -------------------------------------------------------------------------------- 1 | mod basics; 2 | mod data_model; 3 | mod education; 4 | mod publication; 5 | mod shared; 6 | mod supported_languages; 7 | pub mod template; 8 | mod work; 9 | -------------------------------------------------------------------------------- /src/templates/coruscant/publication/index.html: -------------------------------------------------------------------------------- 1 |
2 | {{title}} 3 |
4 | {{entries}} -------------------------------------------------------------------------------- /src/templates/coruscant/publication/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod publication_wrapper; 2 | -------------------------------------------------------------------------------- /src/templates/coruscant/publication/publication_wrapper.rs: -------------------------------------------------------------------------------- 1 | use minijinja::context; 2 | 3 | use crate::templates::coruscant::{ 4 | data_model::{publication::Publication, supported_resume_data::SupportedResumeData}, 5 | shared::{entry::build_entry_single_date, render_template::render_template}, 6 | supported_languages::SupportedLanguages, 7 | }; 8 | 9 | /// Return the publication wrapper as HTML. 10 | pub fn build_publication_wrapper( 11 | resume_data: &SupportedResumeData, 12 | language: &SupportedLanguages, 13 | ) -> String { 14 | if resume_data.publications.is_empty() { 15 | return String::new(); 16 | } 17 | 18 | let rendered_template = render_template( 19 | include_str!("index.html"), 20 | context!(entries => build_entries(&resume_data.publications), title => language.publication_section_title()), 21 | ); 22 | 23 | match rendered_template { 24 | Ok(t) => t, 25 | Err(_) => panic!("Failed to render publication template."), 26 | } 27 | } 28 | 29 | fn build_entries(publications: &Vec) -> String { 30 | let mut entries_html = String::new(); 31 | 32 | for publication in publications { 33 | entries_html.push_str(&build_entry_single_date( 34 | publication.release_date.clone(), 35 | publication.name.clone(), 36 | publication.summary.clone(), 37 | Some(build_footer(publication)), 38 | )); 39 | } 40 | 41 | entries_html 42 | } 43 | 44 | fn build_footer(publication: &Publication) -> String { 45 | format!( 46 | "{} - Link: {}", 47 | publication.publisher.clone(), 48 | publication.url.clone() 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/templates/coruscant/shared/entry.rs: -------------------------------------------------------------------------------- 1 | use minijinja::context; 2 | 3 | use crate::templates::coruscant::shared::render_template::render_template; 4 | 5 | /// Return an entry with select start and end dates as HTML. Entries are the boxes that appear in sections like work or education. 6 | pub fn build_entry_start_and_end( 7 | start_date: String, 8 | end_date: String, 9 | title: String, 10 | body: String, 11 | footer: Option, 12 | ) -> String { 13 | let rendered_template = render_template( 14 | include_str!("index.html"), 15 | context!( 16 | start_date => start_date, 17 | end_date => end_date, 18 | title => title, 19 | body => body, 20 | footer => footer, 21 | ), 22 | ); 23 | 24 | match rendered_template { 25 | Ok(t) => t, 26 | Err(_) => panic!("Failed to render entry template."), 27 | } 28 | } 29 | 30 | /// Return an entry with a singular date as HTML. Entries are the boxes that appear in sections like work or education. 31 | pub fn build_entry_single_date( 32 | date: String, 33 | title: String, 34 | body: String, 35 | footer: Option, 36 | ) -> String { 37 | let rendered_template = render_template( 38 | include_str!("index.html"), 39 | context!( 40 | date => date, 41 | title => title, 42 | body => body, 43 | footer => footer, 44 | ), 45 | ); 46 | 47 | match rendered_template { 48 | Ok(t) => t, 49 | Err(_) => panic!("Failed to render entry template."), 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/templates/coruscant/shared/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {{end_date}} 6 |
7 |
8 | {{date}} 9 |
10 |
11 | {{start_date}} 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{title}} 22 |
23 |
24 | {{body}} 25 |
26 | {% if (footer is defined) and (footer != none) %} 27 | 30 | {% endif %} 31 |
32 |
33 |
-------------------------------------------------------------------------------- /src/templates/coruscant/shared/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod entry; 2 | pub mod render_template; 3 | -------------------------------------------------------------------------------- /src/templates/coruscant/shared/render_template.rs: -------------------------------------------------------------------------------- 1 | use minijinja::Environment; 2 | use serde::Serialize; 3 | 4 | pub fn render_template(template: &str, context: S) -> Result { 5 | let mut environment = Environment::new(); 6 | environment.add_template("", template).unwrap(); 7 | 8 | let template = environment.get_template("").unwrap(); 9 | 10 | match template.render(context) { 11 | Ok(t) => Ok(t), 12 | Err(_) => Err("Could not render template".to_string()), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/templates/coruscant/style.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | height: 100%; 4 | 5 | color: black; 6 | font-family: 'Verdana'; 7 | font-size: 14pt; 8 | } 9 | 10 | @page { 11 | margin-top: 3%; 12 | } 13 | @page :first { 14 | margin-top: 0; 15 | } 16 | 17 | .default-box { 18 | background-color: white; 19 | border-radius: 15pt; 20 | box-shadow: 0 0 25pt rgb(220, 220, 220); 21 | padding: 20pt; 22 | } 23 | 24 | .basics-box { 25 | margin-left: 2.5%; 26 | margin-right: 2.5%; 27 | margin-top: 3%; 28 | height: fit-content; 29 | } 30 | 31 | .basics-wrapper { 32 | display: grid; 33 | gap: 10pt; 34 | grid-template-columns: auto 1fr auto; 35 | } 36 | 37 | .profile-image img { 38 | width: 80pt; 39 | height: 80pt; 40 | border-radius: 50%; 41 | grid-column: 1; 42 | } 43 | 44 | .name-and-label { 45 | grid-column: 2; 46 | align-self: center; 47 | } 48 | 49 | .name { 50 | font-size: 24pt; 51 | font-weight: bold; 52 | margin-bottom: 10pt; 53 | } 54 | 55 | .label { 56 | font-size: 16pt; 57 | } 58 | 59 | .contact-info { 60 | display: grid; 61 | grid-column: 3; 62 | gap: 5pt; 63 | align-items: center; 64 | } 65 | 66 | /* Icon */ 67 | .email-icon, 68 | .phone-icon, 69 | .address-icon { 70 | width: 17pt; 71 | height: 17pt; 72 | } 73 | 74 | .email { 75 | grid-row: 1; 76 | grid-column: 2; 77 | 78 | font-size: 12pt; 79 | } 80 | 81 | .phone-icon { 82 | grid-row: 2; 83 | grid-column: 1; 84 | } 85 | 86 | .phone { 87 | grid-row: 2; 88 | grid-column: 2; 89 | 90 | font-size: 12pt; 91 | } 92 | 93 | .address-icon { 94 | grid-row: 3; 95 | grid-column: 1; 96 | } 97 | 98 | .address { 99 | grid-row: 3; 100 | grid-column: 2; 101 | 102 | font-size: 12pt; 103 | } 104 | 105 | .section-title { 106 | margin-top: 15pt; 107 | margin-left: 125pt; 108 | 109 | font-size: 18pt; 110 | } 111 | 112 | .skills-and-languages-wrapper { 113 | margin-top: 25pt; 114 | 115 | display: grid; 116 | grid-template-columns: 33% 66%; 117 | column-gap: 30pt; 118 | 119 | font-size: 12pt; 120 | } 121 | 122 | .languages-wrapper { 123 | grid-column: 1; 124 | 125 | display: grid; 126 | grid-template-columns: auto 1fr auto; 127 | column-gap: 10pt; 128 | row-gap: 15pt; 129 | align-items: center; 130 | height: fit-content; 131 | } 132 | 133 | .languages-title { 134 | grid-row: 1; 135 | grid-column: 1/3; 136 | } 137 | 138 | .language-name { 139 | grid-column: 1; 140 | } 141 | 142 | .progress-bar { 143 | grid-column: 2; 144 | 145 | height: 5pt; 146 | border-radius: 5pt; 147 | background-color: #e0e0e0; 148 | } 149 | 150 | .progress { 151 | height: 5pt; 152 | background-color: black; 153 | border-radius: 5pt; 154 | } 155 | 156 | .language-fluency { 157 | grid-column: 3; 158 | } 159 | 160 | .skills-wrapper { 161 | grid-column: 2; 162 | } 163 | 164 | .skills-title { 165 | width: 100%; 166 | margin-bottom: 4pt; 167 | } 168 | 169 | .skill { 170 | width: fit-content; 171 | height: 20pt; 172 | margin-top: 5.5pt; 173 | 174 | padding-left: 10pt; 175 | padding-right: 10pt; 176 | padding-top: 4pt; 177 | 178 | border-radius: 20pt; 179 | background-color: #e0e0e0; 180 | display: inline-block; 181 | } 182 | 183 | 184 | .entry { 185 | padding-top: 16pt; 186 | padding-left: 2.5%; 187 | padding-right: 2.5%; 188 | } 189 | 190 | .entry-inner { 191 | display: grid; 192 | grid-template-columns: 1fr auto 82.5%; 193 | gap: 5pt; 194 | 195 | font-size: 12pt; 196 | break-inside: avoid; 197 | break-after: auto; 198 | } 199 | 200 | .entry-inner ul { 201 | margin-top: 5pt; 202 | margin-bottom: 0; 203 | } 204 | 205 | .timespan-column { 206 | grid-column: 1; 207 | justify-self: right; 208 | text-align: right; 209 | position: relative; 210 | 211 | margin-top: 12pt; 212 | margin-bottom: 12pt; 213 | } 214 | 215 | .end-date { 216 | position: absolute; 217 | top: 0; 218 | right: 0; 219 | min-width: 75pt; 220 | } 221 | 222 | .singular-date { 223 | position: absolute; 224 | top: 50%; 225 | transform: translateY(-50%); 226 | right: 0; 227 | min-width: 75pt; 228 | } 229 | 230 | .start-date { 231 | position: absolute; 232 | bottom: 0; 233 | right: 0; 234 | min-width: 75pt; 235 | } 236 | 237 | .timeline { 238 | display: grid; 239 | grid-template-rows: auto 1fr auto; 240 | align-content: center; 241 | padding-top: 16pt; 242 | padding-bottom: 15pt; 243 | } 244 | 245 | .top-circle, .bottom-circle { 246 | width: 8pt; 247 | height: 8pt; 248 | background-color: black; 249 | border-radius: 50%; 250 | z-index: 1; 251 | } 252 | 253 | .top-circle { 254 | grid-row: 1; 255 | grid-column: 1; 256 | } 257 | 258 | .line { 259 | margin-top: 3pt; 260 | margin-bottom: 3pt; 261 | margin-left: 3.25pt; 262 | 263 | grid-row: 1/4; 264 | grid-column: 1; 265 | width: 1.5pt; 266 | background-color: black; 267 | z-index: 1; 268 | } 269 | 270 | .bottom-circle { 271 | grid-row: 3; 272 | grid-column: 1; 273 | } 274 | 275 | .box-column { 276 | grid-column: 3; 277 | height: fit-content; 278 | 279 | background-color: white; 280 | border-radius: 15pt; 281 | box-shadow: -5pt 0 20pt rgb(220, 220, 220); 282 | padding: 12pt; 283 | } 284 | 285 | .entry-title { 286 | font-weight: bold; 287 | 288 | margin-bottom: 10pt; 289 | align-content: start; 290 | } 291 | 292 | .entry-footer { 293 | margin-top: 10pt; 294 | } -------------------------------------------------------------------------------- /src/templates/coruscant/supported_languages.rs: -------------------------------------------------------------------------------- 1 | use crate::GloballySupportedLanguages; 2 | 3 | /// Languages supported by this template. 4 | pub enum SupportedLanguages { 5 | EN, 6 | DE, 7 | } 8 | impl SupportedLanguages { 9 | pub fn try_from(language: &GloballySupportedLanguages) -> Result { 10 | match language { 11 | GloballySupportedLanguages::EN => Ok(Self::EN), 12 | GloballySupportedLanguages::DE => Ok(Self::DE), 13 | } 14 | } 15 | 16 | pub fn languages_section_title(&self) -> String { 17 | match self { 18 | SupportedLanguages::EN => "Languages".to_string(), 19 | SupportedLanguages::DE => "Sprachen".to_string(), 20 | } 21 | } 22 | 23 | pub fn skills_section_title(&self) -> String { 24 | match self { 25 | SupportedLanguages::EN => "Skills".to_string(), 26 | SupportedLanguages::DE => "Kenntnisse".to_string(), 27 | } 28 | } 29 | 30 | pub fn work_section_title(&self) -> String { 31 | match self { 32 | SupportedLanguages::EN => "Experience".to_string(), 33 | SupportedLanguages::DE => "Arbeitserfahrung".to_string(), 34 | } 35 | } 36 | 37 | pub fn education_section_title(&self) -> String { 38 | match self { 39 | SupportedLanguages::EN => "Education".to_string(), 40 | SupportedLanguages::DE => "Bildung".to_string(), 41 | } 42 | } 43 | 44 | pub fn publication_section_title(&self) -> String { 45 | match self { 46 | SupportedLanguages::EN => "Publications".to_string(), 47 | SupportedLanguages::DE => "Veröffentlichungen".to_string(), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/templates/coruscant/template.rs: -------------------------------------------------------------------------------- 1 | use json_resume::Resume; 2 | use minijinja::context; 3 | 4 | use crate::{ 5 | templates::{ 6 | coruscant::publication::publication_wrapper::build_publication_wrapper, template::Template, 7 | }, 8 | GloballySupportedLanguages, 9 | }; 10 | 11 | use super::{ 12 | basics::basics_box::build_basics_wrapper, 13 | data_model::supported_resume_data::SupportedResumeData, 14 | education::education_wrapper::build_education_wrapper, 15 | shared::render_template::render_template, supported_languages::SupportedLanguages, 16 | work::work_wrapper::build_work_wrapper, 17 | }; 18 | 19 | /// A modern, minimalist, and professional resume design. 20 | pub struct Coruscant { 21 | /// Underlying personal data defining the content of the resume (like education, work experience, ...). 22 | resume_data: SupportedResumeData, 23 | /// Language used in the section headers of the resume. 24 | language: SupportedLanguages, 25 | } 26 | impl Template for Coruscant { 27 | fn new( 28 | json_resume_data: Resume, 29 | language: &GloballySupportedLanguages, 30 | ) -> Result { 31 | Ok(Coruscant { 32 | resume_data: SupportedResumeData::try_from(json_resume_data)?, 33 | language: SupportedLanguages::try_from(language)?, 34 | }) 35 | } 36 | 37 | fn build(&self) -> String { 38 | let rendered_template = render_template( 39 | include_str!("index.html"), 40 | context!( 41 | style => include_str!("style.css"), 42 | basics => build_basics_wrapper(&self.resume_data, &self.language), 43 | work => build_work_wrapper(&self.resume_data, &self.language), 44 | education => build_education_wrapper(&self.resume_data, &self.language), 45 | publications => build_publication_wrapper(&self.resume_data, &self.language) 46 | ), 47 | ); 48 | 49 | match rendered_template { 50 | Ok(t) => t, 51 | Err(_) => panic!("Failed to render root template."), 52 | } 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | pub mod tests { 58 | use super::*; 59 | use std::{fs, path::PathBuf}; 60 | 61 | use crate::{generate_pdf, io::load_json_resume::load_json_resume}; 62 | 63 | fn html_is_different( 64 | resume_data_path: &PathBuf, 65 | language: &GloballySupportedLanguages, 66 | html_path: &PathBuf, 67 | ) -> bool { 68 | let generated_html = Coruscant::new(load_json_resume(resume_data_path).unwrap(), language) 69 | .unwrap() 70 | .build(); 71 | 72 | let previous_html = fs::read_to_string(html_path); 73 | 74 | if previous_html.is_ok() && generated_html == previous_html.unwrap() { 75 | return false; 76 | } 77 | 78 | fs::write(html_path, generated_html).unwrap(); 79 | true 80 | } 81 | 82 | #[test] 83 | fn build_example_en() { 84 | let resume_data_path = PathBuf::from("examples/kirk_resume_en.yaml"); 85 | let target_path = PathBuf::from("examples/coruscant_en.pdf"); 86 | let html_path = resume_data_path.parent().unwrap().join("coruscant_en.html"); 87 | let template_enum = crate::AvailableTemplates::Coruscant; 88 | let language = GloballySupportedLanguages::EN; 89 | 90 | if !html_is_different(&resume_data_path, &language, &html_path) { 91 | return; 92 | } 93 | 94 | let _ = fs::remove_file(&target_path); 95 | assert!(!target_path.is_file()); 96 | 97 | generate_pdf( 98 | resume_data_path, 99 | target_path.clone(), 100 | template_enum, 101 | language, 102 | ) 103 | .unwrap(); 104 | assert!(target_path.is_file()); 105 | } 106 | 107 | #[test] 108 | fn build_example_de() { 109 | let resume_data_path = PathBuf::from("examples/kirk_resume_de.yaml"); 110 | let target_path = PathBuf::from("examples/coruscant_de.pdf"); 111 | let html_path = resume_data_path.parent().unwrap().join("coruscant_de.html"); 112 | let template_enum = crate::AvailableTemplates::Coruscant; 113 | let language = GloballySupportedLanguages::DE; 114 | 115 | if !html_is_different(&resume_data_path, &language, &html_path) { 116 | return; 117 | } 118 | 119 | let _ = fs::remove_file(&target_path); 120 | assert!(!target_path.is_file()); 121 | 122 | generate_pdf( 123 | resume_data_path, 124 | target_path.clone(), 125 | template_enum, 126 | language, 127 | ) 128 | .unwrap(); 129 | assert!(target_path.is_file()); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/templates/coruscant/work/index.html: -------------------------------------------------------------------------------- 1 |
2 | {{title}} 3 |
4 | {{entries}} -------------------------------------------------------------------------------- /src/templates/coruscant/work/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod work_wrapper; 2 | -------------------------------------------------------------------------------- /src/templates/coruscant/work/work_wrapper.rs: -------------------------------------------------------------------------------- 1 | use minijinja::context; 2 | 3 | use crate::templates::coruscant::{ 4 | data_model::{supported_resume_data::SupportedResumeData, work::Work}, 5 | shared::{entry::build_entry_start_and_end, render_template::render_template}, 6 | supported_languages::SupportedLanguages, 7 | }; 8 | 9 | /// Return the work wrapper as HTML. 10 | pub fn build_work_wrapper( 11 | resume_data: &SupportedResumeData, 12 | language: &SupportedLanguages, 13 | ) -> String { 14 | if resume_data.work.is_empty() { 15 | return String::new(); 16 | } 17 | 18 | let rendered_template = render_template( 19 | include_str!("index.html"), 20 | context!(entries => build_entries(&resume_data.work), title => language.work_section_title()), 21 | ); 22 | 23 | match rendered_template { 24 | Ok(t) => t, 25 | Err(_) => panic!("Failed to render work template."), 26 | } 27 | } 28 | 29 | fn build_entries(work: &Vec) -> String { 30 | let mut entries_html = String::new(); 31 | 32 | for work_entry in work { 33 | entries_html.push_str(&build_entry_start_and_end( 34 | work_entry.start_date.clone(), 35 | work_entry.end_date.clone(), 36 | work_entry.name.clone(), 37 | build_entry_body(work_entry), 38 | None, 39 | )); 40 | } 41 | 42 | entries_html 43 | } 44 | 45 | fn build_entry_body(work_entry: &Work) -> String { 46 | let mut entry_body = String::new(); 47 | entry_body.push_str(&work_entry.description); 48 | entry_body.push_str("\n
    "); 49 | 50 | for highlight in &work_entry.highlights { 51 | entry_body.push_str(&format!( 52 | " 53 | \n
  • {highlight}
  • 54 | " 55 | )); 56 | } 57 | 58 | entry_body.push_str("\n
"); 59 | 60 | entry_body 61 | } 62 | -------------------------------------------------------------------------------- /src/templates/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod coruscant; 2 | pub mod template; 3 | 4 | pub use crate::templates::coruscant::template::Coruscant; 5 | -------------------------------------------------------------------------------- /src/templates/template.rs: -------------------------------------------------------------------------------- 1 | use json_resume::Resume; 2 | 3 | use crate::GloballySupportedLanguages; 4 | 5 | /// Functions that should be implemented by all templates. 6 | pub trait Template { 7 | /// Construct the template. 8 | fn new(json_resume_data: Resume, language: &GloballySupportedLanguages) -> Result 9 | where 10 | Self: std::marker::Sized; 11 | 12 | /// Return the resume as standalone HTML containing all CSS. 13 | fn build(&self) -> String; 14 | } 15 | --------------------------------------------------------------------------------