diff --git a/docs/assets/images/classic.png b/docs/assets/images/classic.png index d16b28ee..e6207921 100644 Binary files a/docs/assets/images/classic.png and b/docs/assets/images/classic.png differ diff --git a/docs/assets/images/sb2nov.png b/docs/assets/images/sb2nov.png index fb3ade64..2441f35e 100644 Binary files a/docs/assets/images/sb2nov.png and b/docs/assets/images/sb2nov.png differ diff --git a/docs/reference/data/models/rendercv_settings.md b/docs/reference/data/models/rendercv_settings.md new file mode 100644 index 00000000..bbce49c5 --- /dev/null +++ b/docs/reference/data/models/rendercv_settings.md @@ -0,0 +1,3 @@ +# `rendercv.data.models.rendercv_settings` + +::: rendercv.data.models.rendercv_settings \ No newline at end of file diff --git a/docs/user_guide/structure_of_the_yaml_input_file.md b/docs/user_guide/structure_of_the_yaml_input_file.md index ffad9a03..3cf8ecfd 100644 --- a/docs/user_guide/structure_of_the_yaml_input_file.md +++ b/docs/user_guide/structure_of_the_yaml_input_file.md @@ -1,6 +1,6 @@ # Structure of the YAML Input File -RenderCV's input file consists of three parts: `cv`, `design`, and `locale_catalog`. +RenderCV's input file consists of four parts: `cv`, `design`, `locale_catalog` and `rendercv_settings`. ```yaml title="Your_Name_CV.yaml" cv: @@ -15,11 +15,16 @@ locale_catalog: ... TRANSLATIONS TO YOUR LANGUAGE ... +rendercv_settings: + ... + RENDERCV SETTINGS + ... ``` - The `cv` field is mandatory. It contains the **content of the CV**. - The `design` field is optional. It contains the **design options of the CV**. If you don't provide a `design` field, RenderCV will use the default design options with the `classic` theme. - The `locale_catalog` field is optional. You can provide translations for some of the strings used in the CV, for example, month abbreviations. RenderCV will use English strings if you don't provide a `locale_catalog` field. +- The `rendercv_settings` field is optional. It contains the **settings of RenderCV**. If you don't provide a `rendercv_settings` field, RenderCV will use the default settings. !!! tip To maximize your productivity while editing the input YAML file, set up RenderCV's JSON Schema in your IDE. It will validate your inputs on the fly and give auto-complete suggestions. @@ -355,4 +360,22 @@ locale_catalog: ``` 1. The available phone number formats are: `national`, `international`, and `E164`. -2. The `MONTH_ABBREVIATION` and `YEAR` are placeholders. The available placeholders are: `FULL_MONTH_NAME`, `MONTH_ABBREVIATION`, `MONTH`, `MONTH_IN_TWO_DIGITS`, `YEAR`, and `YEAR_IN_TWO_DIGITS`. \ No newline at end of file +2. The `MONTH_ABBREVIATION` and `YEAR` are placeholders. The available placeholders are: `FULL_MONTH_NAME`, `MONTH_ABBREVIATION`, `MONTH`, `MONTH_IN_TWO_DIGITS`, `YEAR`, and `YEAR_IN_TWO_DIGITS`. + +## "`rendercv_settings`" field + +The `rendercv_settings` field contains the settings of RenderCV. This feature is what makes RenderCV a flexible tool. You can change the output folder name, the paths of the output files, and disable the generation of some output files directly from within the code (You can also use the [cli arguments](cli.md) which ever is convinient for you). Below is an example of the `rendercv_settings` field: + +```yaml +rendercv_settings: + output_folder_name: output # default value is 'rendercv_output' + pdf_path: cv.pdf # default value is 'None' + latex_path: cv.tex # default value is 'None' + html_path: cv.html # default value is 'None' + markdown_path: cv.md # default value is 'None' + dont_generate_html: false # default value is 'false' + dont_generate_markdown: false # default value is 'false' + dont_generate_png: false # default value is 'false' +``` + +All this fields are optional. If you don't provide a `rendercv_settings` field, RenderCV will use the default settings. diff --git a/examples/John_Doe_ClassicTheme_CV.pdf b/examples/John_Doe_ClassicTheme_CV.pdf index 1b888aa7..d655b7b7 100644 Binary files a/examples/John_Doe_ClassicTheme_CV.pdf and b/examples/John_Doe_ClassicTheme_CV.pdf differ diff --git a/examples/John_Doe_EngineeringresumesTheme_CV.pdf b/examples/John_Doe_EngineeringresumesTheme_CV.pdf index 3f2f231e..b7db96ef 100644 Binary files a/examples/John_Doe_EngineeringresumesTheme_CV.pdf and b/examples/John_Doe_EngineeringresumesTheme_CV.pdf differ diff --git a/examples/John_Doe_ModerncvTheme_CV.pdf b/examples/John_Doe_ModerncvTheme_CV.pdf index 8882792a..fdd032f7 100644 Binary files a/examples/John_Doe_ModerncvTheme_CV.pdf and b/examples/John_Doe_ModerncvTheme_CV.pdf differ diff --git a/examples/John_Doe_Sb2novTheme_CV.pdf b/examples/John_Doe_Sb2novTheme_CV.pdf index 4eabd8c1..2770d8ad 100644 Binary files a/examples/John_Doe_Sb2novTheme_CV.pdf and b/examples/John_Doe_Sb2novTheme_CV.pdf differ diff --git a/mkdocs.yaml b/mkdocs.yaml index eaea8b4b..965c7800 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -78,6 +78,7 @@ nav: - curriculum_vitae.py: reference/data/models/curriculum_vitae.md - design.py: reference/data/models/design.md - locale_catalog.py: reference/data/models/locale_catalog.md + - rendercv_settings.py: reference/data/models/rendercv_settings.md - rendercv_data_model.py: reference/data/models/rendercv_data_model.md - generator.py: reference/data/generator.md - reader.py: reference/data/reader.md diff --git a/rendercv/cli/commands.py b/rendercv/cli/commands.py index d4bbd132..463a5c38 100644 --- a/rendercv/cli/commands.py +++ b/rendercv/cli/commands.py @@ -5,7 +5,7 @@ import os import pathlib -from typing import Annotated, Literal, Optional +from typing import Annotated, Optional import typer from rich import print @@ -139,59 +139,73 @@ def cli_command_render( # Get paths: input_file_path: pathlib.Path = utilities.string_to_file_path( - input_file_name + string=input_file_name ) # type: ignore - output_directory = pathlib.Path.cwd() / output_folder_name - - paths: dict[ - Literal["latex", "pdf", "markdown", "html", "png"], Optional[pathlib.Path] - ] = { - "latex": utilities.string_to_file_path(latex_path), - "pdf": utilities.string_to_file_path(pdf_path), - "markdown": utilities.string_to_file_path(markdown_path), - "html": utilities.string_to_file_path(html_path), - "png": utilities.string_to_file_path(png_path), + + # dictionary for command line arguments: + cli_args = { + "use_local_latex_command": use_local_latex_command, + "output_folder_name": output_folder_name, + "latex_path": latex_path, + "pdf_path": pdf_path, + "markdown_path": markdown_path, + "html_path": html_path, + "png_path": png_path, + "dont_generate_png": dont_generate_png, + "dont_generate_markdown": dont_generate_markdown, + "dont_generate_html": dont_generate_html, } + # keep the current working directory: + working_directory = pathlib.Path.cwd() + # change the current working directory to the input file's directory (because # the template overrides are looked up in the current working directory): os.chdir(input_file_path.parent) # compute the number of steps - # 1. read and validate the input file + # 0. read the input file + # 1. validate the input file # 2. generate the LaTeX file # 3. render the LaTeX file to a PDF # 4. render PNG files from the PDF # 5. generate the Markdown file # 6. render the Markdown file to a HTML (for Grammarly) + + data_as_a_dict = data.read_a_yaml_file(input_file_path) + + # update the data if there are extra override arguments: + if extra_data_model_override_argumets: + key_and_values = dict() + key_and_values = utilities.parse_render_command_override_arguments( + extra_data_model_override_argumets + ) + data_as_a_dict = utilities.set_or_update_values(data_as_a_dict, key_and_values) + + data_as_a_dict = utilities.parse_render_settings(data_as_a_dict, cli_args) + + rendercv_settings = data_as_a_dict.get("rendercv_settings", dict()) + + # Calculate the number of steps: number_of_steps = 6 - if dont_generate_png: - number_of_steps = number_of_steps - 1 - if dont_generate_markdown: - # if the Markdown file is not generated, then the HTML file is not generated - number_of_steps = number_of_steps - 2 + if rendercv_settings.get("dont_generate_png", False): + number_of_steps -= 1 + if rendercv_settings.get("dont_generate_markdown", False): + number_of_steps -= 2 else: - if dont_generate_html: - number_of_steps = number_of_steps - 1 - - with printer.LiveProgressReporter(number_of_steps) as progress: - progress.start_a_step("Reading and validating the input file") - data_as_a_dict = data.read_a_yaml_file(input_file_path) - - # update the data if there are extra override arguments: - if extra_data_model_override_argumets: - key_and_values = dict() - key_and_values = utilities.parse_render_command_override_arguments( - extra_data_model_override_argumets - ) - data_as_a_dict = utilities.set_or_update_values( - data_as_a_dict, key_and_values - ) + if rendercv_settings.get("dont_generate_html", False): + number_of_steps -= 1 + + with printer.LiveProgressReporter(number_of_steps=number_of_steps) as progress: + progress.start_a_step("Validating the input file") data_model = data.validate_input_dictionary_and_return_the_data_model( data_as_a_dict ) + rendercv_settings = data_model.rendercv_settings + output_directory = working_directory / rendercv_settings.output_folder_name + progress.finish_the_current_step() progress.start_a_step("Generating the LaTeX file") @@ -200,47 +214,70 @@ def cli_command_render( data_model, output_directory ) ) - if paths["latex"]: - utilities.copy_files(latex_file_path_in_output_folder, paths["latex"]) + if rendercv_settings.latex_path: + utilities.copy_files( + latex_file_path_in_output_folder, + utilities.string_to_file_path( + parent=working_directory, string=rendercv_settings.latex_path + ), + ) progress.finish_the_current_step() progress.start_a_step("Rendering the LaTeX file to a PDF") pdf_file_path_in_output_folder = renderer.render_a_pdf_from_latex( latex_file_path_in_output_folder, use_local_latex_command ) - if paths["pdf"]: - utilities.copy_files(pdf_file_path_in_output_folder, paths["pdf"]) + if rendercv_settings.pdf_path: + utilities.copy_files( + pdf_file_path_in_output_folder, + utilities.string_to_file_path( + parent=working_directory, string=rendercv_settings.pdf_path + ), + ) progress.finish_the_current_step() - if not dont_generate_png: + if not rendercv_settings.dont_generate_png: progress.start_a_step("Rendering PNG files from the PDF") png_file_paths_in_output_folder = renderer.render_pngs_from_pdf( pdf_file_path_in_output_folder ) - if paths["png"]: - utilities.copy_files(png_file_paths_in_output_folder, paths["png"]) + if rendercv_settings.png_path: + utilities.copy_files( + png_file_paths_in_output_folder, + utilities.string_to_file_path( + parent=working_directory, string=rendercv_settings.png_path + ), + ) progress.finish_the_current_step() - if not dont_generate_markdown: + if not rendercv_settings.dont_generate_markdown: progress.start_a_step("Generating the Markdown file") markdown_file_path_in_output_folder = renderer.create_a_markdown_file( data_model, output_directory ) - if paths["markdown"]: + if rendercv_settings.markdown_path: utilities.copy_files( - markdown_file_path_in_output_folder, paths["markdown"] + markdown_file_path_in_output_folder, + utilities.string_to_file_path( + parent=working_directory, string=rendercv_settings.markdown_path + ), ) progress.finish_the_current_step() - if not dont_generate_html: + if not rendercv_settings.dont_generate_html: progress.start_a_step( "Rendering the Markdown file to a HTML (for Grammarly)" ) html_file_path_in_output_folder = renderer.render_an_html_from_markdown( markdown_file_path_in_output_folder ) - if paths["html"]: - utilities.copy_files(html_file_path_in_output_folder, paths["html"]) + if rendercv_settings.html_path: + utilities.copy_files( + html_file_path_in_output_folder, + utilities.string_to_file_path( + parent=working_directory, string=rendercv_settings.html_path + ), + ) progress.finish_the_current_step() diff --git a/rendercv/cli/utilities.py b/rendercv/cli/utilities.py index c24cf4eb..2101790a 100644 --- a/rendercv/cli/utilities.py +++ b/rendercv/cli/utilities.py @@ -2,6 +2,7 @@ The `rendercv.cli.utilities` module contains utility functions that are required by CLI. """ +import inspect import json import pathlib import re @@ -12,18 +13,26 @@ import typer -def string_to_file_path(string: Optional[str]) -> Optional[pathlib.Path]: +def string_to_file_path( + string: Optional[str], parent: Optional[str] = None +) -> Optional[pathlib.Path]: """Convert a string to a pathlib.Path object. If the string is None, then return None. Args: + parent (Optional[str]): The parent directory of the file path. string (str): The string to be converted to a pathlib.Path object. Returns: pathlib.Path: The pathlib.Path object. """ + # check if the string is not None: if string is not None: - return pathlib.Path(string).absolute() + # check if the parent is not None: + if parent is not None: + return pathlib.Path(parent).absolute() / string + else: + return pathlib.Path(string).absolute() else: return None @@ -260,3 +269,85 @@ def parse_render_command_override_arguments( key_and_values[key] = value return key_and_values + + +def update_render_settings( + dictionary: dict, + arguments: dict[str, str], + arguments_default_values: dict[str, str], +) -> dict[str, str]: + """Build the RenderCV settings dictionary by combining the dictionary and the + command line arguments. + + Args: + dictionary (dict): The dictionary to be combined with the command line + arguments. + arguments (dict[str, str]): The command line arguments. + arguments_default_values (dict[str, str]): The default values of + the command line arguments. + + Returns: + dict[str, str]: The combined dictionary. + """ + + # if the dictionary is empty, initialize it from the default values: + if not dictionary: + dictionary = arguments_default_values + + # Combine the dictionary and the command line arguments if the values are not None: + for key, value in arguments.items(): + # check if the key is present in the both + # command line arguments and the default values: + if key in arguments_default_values: + default_value = arguments_default_values[key] + if value != default_value: + dictionary = set_or_update_a_value(dictionary, key, str(value)) + else: + # The key is not present in the default values, set the value: + # throw an error reporting this + raise ValueError( + f"The key ({key}) is not present in the default values of the command" + " line arguments!" + ) + return dictionary + + +def parse_render_settings( + dictionary: dict, + cli_arguments: dict[str, str], +) -> dict[str, str]: + """Build the RenderCV settings dictionary by combining the dictionary and the + command line arguments. + + Args: + dictionary (dict): The dictionary to be combined with the command line + arguments. + cli_arguments (dict[str, str]): The command line arguments. + + Returns: + dict[str, str]: The combined dictionary. + """ + + # Use inspect to get the default values of the arguments: + from .commands import cli_command_render + + sig = inspect.signature(cli_command_render) + cli_arguments_default = { + k: v.default + for k, v in sig.parameters.items() + if v.default is not inspect.Parameter.empty + } + + # update the data of the rendercv settings: + rendercv_settings = dictionary.get("rendercv_settings", dict()) + if rendercv_settings is None: + rendercv_settings = dict() + + rendercv_settings = update_render_settings( + rendercv_settings, cli_arguments, cli_arguments_default + ) + + # update the data model with the rendercv settings: + dictionary["rendercv_settings"] = rendercv_settings + + return dictionary diff --git a/rendercv/data/models/rendercv_data_model.py b/rendercv/data/models/rendercv_data_model.py index 7be1dda2..53a33854 100644 --- a/rendercv/data/models/rendercv_data_model.py +++ b/rendercv/data/models/rendercv_data_model.py @@ -12,6 +12,7 @@ from .curriculum_vitae import CurriculumVitae from .design import RenderCVDesign from .locale_catalog import LocaleCatalog +from .rendercv_settings import RenderCVSettings class RenderCVDataModel(RenderCVBaseModelWithoutExtraKeys): @@ -36,6 +37,12 @@ class RenderCVDataModel(RenderCVBaseModelWithoutExtraKeys): ), validate_default=True, ) + rendercv_settings: Optional[RenderCVSettings] = pydantic.Field( + default=None, + title="RenderCV Settings", + validate_default=True, + description="The settings of the RenderCV.", + ) @pydantic.field_validator("locale_catalog") @classmethod @@ -46,3 +53,15 @@ def initialize_locale_catalog(cls, locale_catalog: LocaleCatalog) -> LocaleCatal LocaleCatalog() return locale_catalog + + @pydantic.field_validator("rendercv_settings") + @classmethod + def initialize_rendercv_settings( + cls, rendercv_settings: RenderCVSettings + ) -> RenderCVSettings: + """Even if the rendercv settings are not provided, initialize them with + the default values.""" + if rendercv_settings is None: + RenderCVSettings() + + return rendercv_settings diff --git a/rendercv/data/models/rendercv_settings.py b/rendercv/data/models/rendercv_settings.py new file mode 100644 index 00000000..f216ba65 --- /dev/null +++ b/rendercv/data/models/rendercv_settings.py @@ -0,0 +1,132 @@ +""" +The `rendercv.models.rendercv_settings` module contains the data model of the +`rendercv_settings` field of the input file. +""" + +from typing import Optional + +import pydantic + +from .base import RenderCVBaseModelWithExtraKeys + + +class RenderCVSettings(RenderCVBaseModelWithExtraKeys): + """This class is the data model of the rendercv settings. The values of each field + updates the `rendercv_settings` dictionary. + """ + + output_folder_name: Optional[str] = pydantic.Field( + default="rendercv_output", + title="Output Folder Name", + description=( + "The name of the folder where the output files will be saved. The default" + ' value is "rendercv_output".' + ), + ) + + use_local_latex_command: Optional[str] = pydantic.Field( + default=None, + title="Local LaTeX Command", + description=( + "The command to compile the LaTeX file to a PDF file. The default value is" + ' "pdflatex".' + ), + ) + + pdf_path: Optional[str] = pydantic.Field( + default=None, + title="PDF Path", + description=( + "The path of the PDF file. If it is not provided, the PDF file will not be" + " generated. The default value is an empty string." + ), + ) + + latex_path: Optional[str] = pydantic.Field( + default=None, + title="LaTeX Path", + description=( + "The path of the LaTeX file. If it is not provided, the LaTeX file will not" + " be generated. The default value is an empty string." + ), + ) + + html_path: Optional[str] = pydantic.Field( + default=None, + title="HTML Path", + description=( + "The path of the HTML file. If it is not provided, the HTML file will not" + " be generated. The default value is an empty string." + ), + ) + + png_path: Optional[str] = pydantic.Field( + default=None, + title="PNG Path", + description=( + "The path of the PNG file. If it is not provided, the PNG file will not be" + " generated. The default value is an empty string." + ), + ) + + markdown_path: Optional[str] = pydantic.Field( + default=None, + title="Markdown Path", + description=( + "The path of the Markdown file. If it is not provided, the Markdown file" + " will not be generated. The default value is an empty string." + ), + ) + + dont_generate_html: Optional[bool] = pydantic.Field( + default=False, + title="Generate HTML Flag", + description=( + "A boolean value to determine whether the HTML file will be generated. The" + " default value is False." + ), + ) + + dont_generate_markdown: Optional[bool] = pydantic.Field( + default=False, + title="Generate Markdown Flag", + description=( + "A boolean value to determine whether the Markdown file will be generated." + " The default value is False." + ), + ) + + dont_generate_png: Optional[bool] = pydantic.Field( + default=False, + title="Generate PNG Flag", + description=( + "A boolean value to determine whether the PNG file will be generated. The" + " default value is False." + ), + ) + + @pydantic.field_validator( + "output_folder_name", + "pdf_path", + "latex_path", + "html_path", + "png_path", + "markdown_path", + "dont_generate_html", + "dont_generate_markdown", + "dont_generate_png", + ) + @classmethod + def update_settings( + cls, value: Optional[str], info: pydantic.ValidationInfo + ) -> Optional[str]: + """Update the `rendercv_settings` dictionary with the provided values.""" + if value: + rendercv_settings[info.field_name] = value # type: ignore + + return value + + +# Initialize the rendercv settings with the default values +rendercv_settings: dict[str, str] = {} +RenderCVSettings() # Initialize the rendercv settings with the default values diff --git a/rendercv/renderer/renderer.py b/rendercv/renderer/renderer.py index b54d2684..8ea32acd 100644 --- a/rendercv/renderer/renderer.py +++ b/rendercv/renderer/renderer.py @@ -12,7 +12,6 @@ from typing import Optional import fitz - import markdown from .. import data diff --git a/schema.json b/schema.json index 219cd31e..250977c8 100644 --- a/schema.json +++ b/schema.json @@ -1687,6 +1687,114 @@ "title": "PublicationEntry", "type": "object" }, + "RenderCVSettings": { + "additionalProperties": false, + "description": "This class is the data model of the rendercv settings. The values of each field\nupdates the `rendercv_settings` dictionary.", + "properties": { + "output_folder_name": { + "default": "rendercv_output", + "description": "The name of the folder where the output files will be saved. The default value is \"rendercv_output\".", + "title": "Output Folder Name", + "oneOf": [ + { + "type": "string" + } + ] + }, + "use_local_latex_command": { + "default": null, + "description": "The command to compile the LaTeX file to a PDF file. The default value is \"pdflatex\".", + "title": "Local LaTeX Command", + "oneOf": [ + { + "type": "string" + } + ] + }, + "pdf_path": { + "default": null, + "description": "The path of the PDF file. If it is not provided, the PDF file will not be generated. The default value is an empty string.", + "title": "PDF Path", + "oneOf": [ + { + "type": "string" + } + ] + }, + "latex_path": { + "default": null, + "description": "The path of the LaTeX file. If it is not provided, the LaTeX file will not be generated. The default value is an empty string.", + "title": "LaTeX Path", + "oneOf": [ + { + "type": "string" + } + ] + }, + "html_path": { + "default": null, + "description": "The path of the HTML file. If it is not provided, the HTML file will not be generated. The default value is an empty string.", + "title": "HTML Path", + "oneOf": [ + { + "type": "string" + } + ] + }, + "png_path": { + "default": null, + "description": "The path of the PNG file. If it is not provided, the PNG file will not be generated. The default value is an empty string.", + "title": "PNG Path", + "oneOf": [ + { + "type": "string" + } + ] + }, + "markdown_path": { + "default": null, + "description": "The path of the Markdown file. If it is not provided, the Markdown file will not be generated. The default value is an empty string.", + "title": "Markdown Path", + "oneOf": [ + { + "type": "string" + } + ] + }, + "dont_generate_html": { + "default": false, + "description": "A boolean value to determine whether the HTML file will be generated. The default value is False.", + "title": "Generate HTML Flag", + "oneOf": [ + { + "type": "boolean" + } + ] + }, + "dont_generate_markdown": { + "default": false, + "description": "A boolean value to determine whether the Markdown file will be generated. The default value is False.", + "title": "Generate Markdown Flag", + "oneOf": [ + { + "type": "boolean" + } + ] + }, + "dont_generate_png": { + "default": false, + "description": "A boolean value to determine whether the PNG file will be generated. The default value is False.", + "title": "Generate PNG Flag", + "oneOf": [ + { + "type": "boolean" + } + ] + } + }, + "title": "RenderCVSettings", + "type": "object" + }, "Sb2novThemeOptions": { "additionalProperties": false, "description": "This class is the data model of the theme options for the `sb2nov` theme.", @@ -2002,6 +2110,19 @@ "default": null, "description": "The locale catalog of the CV to allow the support of multiple languages.", "title": "Locale Catalog" + }, + "rendercv_settings": { + "anyOf": [ + { + "$ref": "#/$defs/RenderCVSettings" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The settings of the RenderCV.", + "title": "RenderCV Settings" } }, "required": [