diff --git a/.gitignore b/.gitignore index 6e516b3..7405de3 100644 --- a/.gitignore +++ b/.gitignore @@ -170,8 +170,9 @@ cython_debug/ *.pdf # TinyTeX binaries -rendercv/tinytex/vendor/TinyTeX/ +rendercv/vendor/TinyTeX/ # RenderCV related tests/outputs/ -tests/inputs/personal.json \ No newline at end of file +tests/inputs/personal.json +tests/inputs/personal.yaml \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..662b313 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true, + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + } + ] +} diff --git a/rendercv/__main__.py b/rendercv/__main__.py new file mode 100644 index 0000000..45f2b01 --- /dev/null +++ b/rendercv/__main__.py @@ -0,0 +1,73 @@ +import os +import json +import logging +import re + +from jinja2 import Environment, FileSystemLoader + +from ruamel.yaml import YAML + +from rendercv.data_model import RenderCVDataModel +from rendercv.tinytex import run_latex + +if __name__ == "__main__": + # logging config: + logging.basicConfig( + level=logging.DEBUG, + format="%(name)s - %(levelname)s - %(message)s", + ) + + workspace = os.path.dirname(os.path.dirname(__file__)) + templateName = "classic" + templatePath = os.path.join(workspace, "rendercv", "templates", templateName) + environment = Environment( + loader=FileSystemLoader(templatePath), + trim_blocks=True, + lstrip_blocks=True, + ) + environment.globals.update(str=str) + + def markdown_to_latex(value: str) -> str: + """ + To be continued... + """ + # convert links + link = re.search("\[(.*)\]\((.*?)\)", value) + if link is not None: + link = link.groups() + oldLinkString = "[" + link[0] + "](" + link[1] + ")" + newLinkString = "\hrefExternal{" + link[1] + "}{" + link[0] + "}" + + value = value.replace(oldLinkString, newLinkString) + + return value + + environment.filters["markdown_to_latex"] = markdown_to_latex + + environment.block_start_string = "((*" + environment.block_end_string = "*))" + environment.variable_start_string = "<<" + environment.variable_end_string = ">>" + environment.comment_start_string = "((#" + environment.comment_end_string = "#))" + + template = environment.get_template(f"{templateName}.tex.j2") + + inpur_name = "personal" + + input_file_path = os.path.join(workspace, "tests", "inputs", f"{inpur_name}.yaml") + with open(input_file_path) as file: + yaml = YAML() + raw_json = yaml.load(file) + + data = RenderCVDataModel(**raw_json) + + output_latex_file = template.render(design=data.design.options, cv=data.cv) + + # Create an output file and write the rendered LaTeX code to it: + output_file_path = os.path.join(workspace, "tests", "outputs", f"{inpur_name}.tex") + os.makedirs(os.path.dirname(output_file_path), exist_ok=True) + with open(output_file_path, "w") as file: + file.write(output_latex_file) + + run_latex(output_file_path) diff --git a/rendercv/data_model.py b/rendercv/data_model.py new file mode 100644 index 0000000..392f88f --- /dev/null +++ b/rendercv/data_model.py @@ -0,0 +1,409 @@ +from datetime import date as Date +from typing import Literal +from typing_extensions import Annotated +import re +import logging +import math +from functools import cached_property + +from pydantic import BaseModel, HttpUrl, Field, model_validator, computed_field +from pydantic.functional_validators import AfterValidator +from pydantic_extra_types.phone_numbers import PhoneNumber +from pydantic_extra_types.color import Color + +from spellchecker import SpellChecker + +# ====================================================================================== +# HELPERS ============================================================================== +# ====================================================================================== + +spell = SpellChecker() + +# don't give spelling warnings for these words: +dictionary = [ + "aerostructures", + "sportsperson", + "cern", + "mechatronics", + "calculix", + "microcontroller", + "ansys", + "nx", + "aselsan", + "hrjet", + "simularge", + "siemens", + "dynamometer", + "dc", +] + + +def check_spelling(sentence: str) -> str: + """ + To be continued... + """ + modifiedSentence = sentence.lower() # convert to lower case + modifiedSentence = re.sub( + r"\-+", " ", modifiedSentence + ) # replace hyphens with spaces + modifiedSentence = re.sub( + "[^a-z\s\-']", "", modifiedSentence + ) # remove all the special characters + words = modifiedSentence.split() # split sentence into a list of words + misspelled = spell.unknown(words) # find misspelled words + + if len(misspelled) > 0: + for word in misspelled: + # for each misspelled word, check if it is in the dictionary and otherwise + # give a warning + if word in dictionary: + continue + + logging.warning( + f'The word "{word}" might be misspelled according to the' + " pyspellchecker." + ) + + return sentence + + +SpellCheckedString = Annotated[str, AfterValidator(check_spelling)] + + +def compute_time_span_string(start_date: Date, end_date: Date) -> str: + """ + To be continued... + """ + # calculate the number of days between start_date and end_date: + timeSpanInDays = (end_date - start_date).days + + # calculate the number of years between start_date and end_date: + howManyYears = timeSpanInDays // 365 + if howManyYears == 0: + howManyYearsString = None + elif howManyYears == 1: + howManyYearsString = "1 year" + else: + howManyYearsString = f"{howManyYears} years" + + # calculate the number of months between start_date and end_date: + howManyMonths = round((timeSpanInDays % 365) / 30) + if howManyMonths == 0: + howManyMonths = 1 + + if howManyMonths == 0: + howManyMonthsString = None + elif howManyMonths == 1: + howManyMonthsString = "1 month" + else: + howManyMonthsString = f"{howManyMonths} months" + + # combine howManyYearsString and howManyMonthsString: + if howManyYearsString is None: + timeSpanString = howManyMonthsString + elif howManyMonthsString is None: + timeSpanString = howManyYearsString + else: + timeSpanString = f"{howManyYearsString} {howManyMonthsString}" + + return timeSpanString + + +def format_date(date: Date) -> str: + """ + To be continued... + """ + # Month abbreviations, + # taken from: https://web.library.yale.edu/cataloging/months + abbreviations_of_months = [ + "Jan.", + "Feb.", + "Mar.", + "Apr.", + "May", + "June", + "July", + "Aug.", + "Sept.", + "Oct.", + "Nov.", + "Dec.", + ] + + month = int(date.strftime("%m")) + monthAbbreviation = abbreviations_of_months[month - 1] + year = date.strftime("%Y") + date_string = f"{monthAbbreviation} {year}" + + return date_string + + +# ====================================================================================== +# ====================================================================================== +# ====================================================================================== + + +# ====================================================================================== +# DESIGN MODELS ======================================================================== +# ====================================================================================== + + +class ClassicThemeOptions(BaseModel): + """ + In RenderCV, each theme has its own ThemeNameThemeOptions class so that new themes + can be implemented easily. + """ + + primary_color: Color = Field(default="blue") + + page_top_margin: str = Field(default="1.35cm") + page_bottom_margin: str = Field(default="1.35cm") + page_left_margin: str = Field(default="1.35cm") + page_right_margin: str = Field(default="1.35cm") + + section_title_top_margin: str = Field(default="0.13cm") + section_title_bottom_margin: str = Field(default="0.13cm") + + vertical_margin_between_bullet_points: str = Field(default="0.07cm") + bullet_point_left_margin: str = Field(default="0.7cm") + + vertical_margin_between_entries: str = Field(default="0.12cm") + + vertical_margin_between_entries_and_highlights: str = Field(default="0.12cm") + + date_and_location_width: str = Field(default="3.7cm") + + +class Design(BaseModel): + theme: Literal["classic"] = "classic" + options: ClassicThemeOptions + + +# ====================================================================================== +# ====================================================================================== +# ====================================================================================== + +# ====================================================================================== +# CONTENT MODELS ======================================================================= +# ====================================================================================== + + +class Skill(BaseModel): + # 1) Mandotory user inputs: + name: str + # 2) Optional user inputs: + details: str = None + + +class Event(BaseModel): + start_date: Date = None + end_date: Date | Literal["present"] = None + date: str | Date = None + location: str = None + highlights: list[SpellCheckedString] = None + + @model_validator(mode="after") + @classmethod + def check_dates(cls, model): + """ + To be continued... + """ + if ( + model.start_date is not None + and model.end_date is not None + and model.date is not None + ): + logging.warning( + "start_date, end_date and date are all provided. Therefore, date will" + " be ignored." + ) + model.date = None + elif model.date is not None and ( + model.start_date is not None or model.end_date is not None + ): + logging.warning( + "date is provided. Therefore, start_date and end_date will be ignored." + ) + model.start_date = None + model.end_date = None + + return model + + @computed_field + @cached_property + def date_and_location_strings(self) -> list[str]: + date_and_location_strings = [] + + if self.location is not None: + date_and_location_strings.append(self.location) + + if self.date is not None: + # Then it means start_date and end_date are not provided. + date_and_location_strings.append(self.date) + else: + # Then it means start_date and end_date are provided. + + start_date = format_date(self.start_date) + + if self.end_date == "present": + end_date = "present" + + time_span_string = compute_time_span_string( + self.start_date, Date.today() + ) + else: + end_date = format_date(self.end_date) + + time_span_string = compute_time_span_string( + self.start_date, self.end_date + ) + + date_and_location_strings.append(f"{start_date} to {end_date}") + + list_of_no_time_span_string_classes = [ + "Education", + ] + if not self.__class__.__name__ in list_of_no_time_span_string_classes: + date_and_location_strings.append(f"{time_span_string}") + + return date_and_location_strings + + @computed_field + @cached_property + def highlight_strings(self) -> list[SpellCheckedString]: + """ + To be continued... + """ + highlight_strings = [] + + highlight_strings.extend(self.highlights) + + return highlight_strings + + +class TestScore(Event): + # 1) Mandotory user inputs: + name: str + score: str + # 2) Optional user inputs: + url: HttpUrl = None + + +class NormalEntry(Event): + # 1) Mandotory user inputs: + name: str + # 2) Optional user inputs: + url: HttpUrl = None + + @computed_field + @cached_property + def highlight_strings(self) -> list[SpellCheckedString]: + """ + To be continued... + """ + highlight_strings = [] + + highlight_strings.extend(self.highlights) + + if self.url is not None: + # remove "https://" from the url for a cleaner look + textUrl = str(self.url).replace("https://", "") + linkString = f"Course certificate: [{textUrl}]({self.transcript_url}))" + highlight_strings.append(linkString) + + return highlight_strings + + +class ExperienceEntry(Event): + # 1) Mandotory user inputs: + company: str + position: str + # 2) Optional user inputs: + + +class EducationEntry(Event): + # 1) Mandotory user inputs: + institution: str + area: str + # 2) Optional user inputs: + study_type: str = None + gpa: str = None + transcript_url: HttpUrl = None + + @computed_field + @cached_property + def highlight_strings(self) -> list[SpellCheckedString]: + """ + To be continued... + """ + highlight_strings = [] + + if self.gpa is not None: + gpaString = f"GPA: {self.gpa}" + if self.transcript_url is not None: + gpaString += f" ([Transcript]({self.transcript_url}))" + highlight_strings.append(gpaString) + + highlight_strings.extend(self.highlights) + + return highlight_strings + + +class SocialNetwork(BaseModel): + # 1) Mandotory user inputs: + network: Literal["LinkedIn", "GitHub", "Instagram"] + username: str + + +class Connection(BaseModel): + # 3) Derived fields (not user inputs): + name: Literal["LinkedIn", "GitHub", "Instagram", "phone", "email", "website"] + value: str + + +class CurriculumVitae(BaseModel): + # 1) Mandotory user inputs: + name: str + # 2) Optional user inputs: + email: str = None + phone: PhoneNumber = None + website: HttpUrl = None + location: str = None + social_networks: list[SocialNetwork] = None + education: list[EducationEntry] = None + work_experience: list[ExperienceEntry] = None + academic_projects: list[NormalEntry] = None + certificates: list[NormalEntry] = None + extracurricular_activities: list[ExperienceEntry] = None + test_scores: list[TestScore] = None + skills: list[Skill] = None + + @computed_field + @cached_property + def connections(self) -> list[str]: + connections = [] + if self.phone is not None: + connections.append(Connection(name="phone", value=self.phone)) + if self.email is not None: + connections.append(Connection(name="email", value=self.email)) + if self.website is not None: + connections.append(Connection(name="website", value=str(self.website))) + if self.social_networks is not None: + for social_network in self.social_networks: + connections.append( + Connection( + name=social_network.network, value=social_network.username + ) + ) + + return connections + + +# ====================================================================================== +# ====================================================================================== +# ====================================================================================== + + +class RenderCVDataModel(BaseModel): + design: Design + cv: CurriculumVitae diff --git a/rendercv/templates/classic/classic.tex.j2 b/rendercv/templates/classic/classic.tex.j2 index 822b44e..8c0a714 100644 --- a/rendercv/templates/classic/classic.tex.j2 +++ b/rendercv/templates/classic/classic.tex.j2 @@ -1,6 +1,7 @@ ((# IMPORT MACROS #)) -((* from "components/education.tex.j2" import education with context *)) -((* from "components/experience.tex.j2" import experience with context *)) +((* from "components/education_entry.tex.j2" import education_entry with context *)) +((* from "components/experience_entry.tex.j2" import experience_entry with context *)) +((* from "components/normal_entry.tex.j2" import normal_entry with context *)) ((* from "components/header.tex.j2" import header with context *)) \documentclass[10pt, a4paper]{memoir} @@ -94,27 +95,65 @@ \newcommand{\hrefExternal}[2]{\href{#1}{#2\, \raisebox{.1ex}{\footnotesize \faExternalLink*}}} % new command for external links \begin{document} - % Test out the 4 main entry types with the commands below: <> \section{Education} - ((* for edu in cv.education *)) - <> - ((* endfor *)) + ((* if not loop.last *)) + \vspace*{<>} + + ((* endif *)) +((* endfor *)) \section{Work Experience} - ((* for work in cv.work_experience *)) - <> - ((* endfor *)) + ((* if not loop.last *)) + \vspace*{<>} + + ((* endif *)) +((* endfor *)) + + \section{Academic Projects} +((* for academic_project in cv.academic_projects *)) + <> + ((* if not loop.last *)) + \vspace*{<>} + + ((* endif *)) +((* endfor *)) + + \section{Certificates} +((* for certificate in cv.certificates *)) + <> + ((* if not loop.last *)) + \vspace*{<>} + + ((* endif *)) +((* endfor *)) + \end{document} \ No newline at end of file diff --git a/rendercv/templates/classic/components/education_entry.tex.j2 b/rendercv/templates/classic/components/education_entry.tex.j2 new file mode 100644 index 0000000..ca97fbe --- /dev/null +++ b/rendercv/templates/classic/components/education_entry.tex.j2 @@ -0,0 +1,19 @@ +((* from "components/highlights.tex.j2" import highlights as print_higlights with context *)) +((* from "components/date_and_location_strings.tex.j2" import date_and_location_strings as print_date_and_locations *)) + +((* macro education_entry(study_type, institution, area, highlights, date_and_location_strings)*)) +((# \begin{tabularx}{⟨width⟩}[⟨pos⟩]{⟨preamble⟩} #)) + ((# width: \textwidth #)) + ((# preamble: first column, second column, third column #)) + ((# first column: p{0.55cm}; constant width, ragged left column #)) + ((# second column: X; variable width, ragged left column #)) + ((# third column: R{<>}; constant widthm ragged right column #)) +\begin{tabularx}{\textwidth}{p{0.55cm} X R{<>}} + \textbf{<>} + & + \textbf{<>}, <> + <> + & + <> +\end{tabularx} +((* endmacro *)) \ No newline at end of file diff --git a/rendercv/templates/classic/components/experience_entry.tex.j2 b/rendercv/templates/classic/components/experience_entry.tex.j2 new file mode 100644 index 0000000..7e868b5 --- /dev/null +++ b/rendercv/templates/classic/components/experience_entry.tex.j2 @@ -0,0 +1,16 @@ +((* from "components/highlights.tex.j2" import highlights as print_higlights with context *)) +((* from "components/date_and_location_strings.tex.j2" import date_and_location_strings as print_date_and_locations *)) + +((* macro experience_entry(company, position, highlights, date_and_location_strings)*)) +((# \begin{tabularx}{⟨width⟩}[⟨pos⟩]{⟨preamble⟩} #)) + ((# width: \textwidth #)) + ((# preamble: first column, second column #)) + ((# first column:: X; variable width, ragged left column #)) + ((# second column: R{<>}; constant width ragged right column #)) +\begin{tabularx}{\textwidth}{X R{<>}} + \textbf{<>}, <> + <> + & + <> +\end{tabularx} +((* endmacro *)) \ No newline at end of file diff --git a/rendercv/templates/classic/components/header.tex.j2 b/rendercv/templates/classic/components/header.tex.j2 index 673f0ec..5952510 100644 --- a/rendercv/templates/classic/components/header.tex.j2 +++ b/rendercv/templates/classic/components/header.tex.j2 @@ -11,6 +11,5 @@ \hspace{0.5cm} ((* endif *)) ((* endfor *)) - \end{header} ((* endmacro *)) \ No newline at end of file diff --git a/rendercv/templates/classic/components/highlights.tex.j2 b/rendercv/templates/classic/components/highlights.tex.j2 index 55df513..e77d5da 100644 --- a/rendercv/templates/classic/components/highlights.tex.j2 +++ b/rendercv/templates/classic/components/highlights.tex.j2 @@ -4,5 +4,5 @@ ((* for item in highlights *)) \item <> ((* endfor *)) -\end{highlights} +\end{highlights} ((* endmacro *)) \ No newline at end of file diff --git a/rendercv/templates/classic/components/normal_entry.tex.j2 b/rendercv/templates/classic/components/normal_entry.tex.j2 new file mode 100644 index 0000000..d64eade --- /dev/null +++ b/rendercv/templates/classic/components/normal_entry.tex.j2 @@ -0,0 +1,21 @@ +((* from "components/highlights.tex.j2" import highlights as print_higlights with context *)) +((* from "components/date_and_location_strings.tex.j2" import date_and_location_strings as print_date_and_locations *)) + +((* macro normal_entry(name, highlights, date_and_location_strings, url, urlText)*)) +((# \begin{tabularx}{⟨width⟩}[⟨pos⟩]{⟨preamble⟩} #)) + ((# width: \textwidth #)) + ((# preamble: first column, second column #)) + ((# first column:: X; variable width, ragged left column #)) + ((# second column: R{<>}; constant width ragged right column #)) +\begin{tabularx}{\textwidth}{X R{<>}} + ((* if url is not none *)) + ((* set markdownUrl = "["+urlText+"]("+ str(url) +")" *)) + \textbf{<>}, <> + ((* else *)) + \textbf{<>} + ((* endif *)) + <> + & + <> +\end{tabularx} +((* endmacro *)) \ No newline at end of file diff --git a/rendercv/tinytex.py b/rendercv/tinytex.py new file mode 100644 index 0000000..921338a --- /dev/null +++ b/rendercv/tinytex.py @@ -0,0 +1,35 @@ +import os +import subprocess + +def run_latex(latexFilePath): + latexFilePath = os.path.normpath(latexFilePath) + latexFile = os.path.basename(latexFilePath) + + if os.name == "nt": + # remove all files except the .tex file + for file in os.listdir(os.path.dirname(latexFilePath)): + if file.endswith(".tex"): + continue + os.remove(os.path.join(os.path.dirname(latexFilePath), file)) + + tinytexPath = os.path.join( + os.path.dirname(__file__), + "vendor", + "TinyTeX", + "bin", + "windows", + ) + subprocess.run( + [ + f"{tinytexPath}\\latexmk.exe", + "-lualatex", + # "-c", + f"{latexFile}", + "-synctex=1", + "-interaction=nonstopmode", + "-file-line-error", + ], + cwd=os.path.dirname(latexFilePath), + ) + else: + print("Only Windows is supported for now.") diff --git a/rendercv/vendor/README.md b/rendercv/vendor/README.md new file mode 100644 index 0000000..8a2e69f --- /dev/null +++ b/rendercv/vendor/README.md @@ -0,0 +1,46 @@ +# TinyTeX + +Normally, this directory should contain a `TinyTeX` folder with all the TinyTeX binaries. I couldn't figure out how to get a minimal TinyTeX that can render a CV and have a small file size, so I don't push TinyTeX to Github for now. + +## Modifications that have been made to TinyTeX +Attempts to make TinyTeX smaller for CV purposes only. + +### FontAwesome + +- mkdir TinyTeX/textmf-local/fonts/opentype/FontAwesome and copy FontAwesome's opentype contents inside. +- mkdir TinyTeX/textmf-local/tex/latex/FontAwesome and copy FontAwesome's tex contents inside. + +### SourceSans3 Font +- mkdir TinyTeX\texmf-local\fonts\truetype and copy all ttf files inside. + +### Removing files + +- In TinyTeX/, remove all the files except LICENSE.CTAN and LICENSE.TL +- Remove TinyTeX/tlpg/ +- Remove TinyTeX/texmf-var +- Remove TinyTeX/texmf-config +- Remove TinyTeX/texmf-dist/fonts/opentype +- Remove TinyTeX/texmf-dist/fonts/type1 +- Remove TinyTeX/texmf-dist/fonts/afm +- Remove TinyTeX/texmf-dist/fonts/tfm +- Remove TinyTeX/texmf-dist/doc +- Remove TinyTeX/texmf-dist/bibtex + +Remove all below: +[Title](TinyTeX/bin/windows/kpsestat.exe) [Title](TinyTeX/bin/windows/kpsewhich.exe) [Title](TinyTeX/bin/windows/afm2tfm.exe) [Title](TinyTeX/bin/windows/biber.exe) [Title](TinyTeX/bin/windows/bibtex.exe) [Title](TinyTeX/bin/windows/dvilualatex.exe) [Title](TinyTeX/bin/windows/dviluatex.exe) [Title](TinyTeX/bin/windows/dvipdfm.exe) [Title](TinyTeX/bin/windows/dvipdfmx.exe) [Title](TinyTeX/bin/windows/dvips.exe) [Title](TinyTeX/bin/windows/ebb.exe) [Title](TinyTeX/bin/windows/eps2eps.exe) [Title](TinyTeX/bin/windows/epstopdf.exe) [Title](TinyTeX/bin/windows/etex.exe) [Title](TinyTeX/bin/windows/extractbb.exe) [Title](TinyTeX/bin/windows/fc-cache.exe) [Title](TinyTeX/bin/windows/fc-cat.exe) [Title](TinyTeX/bin/windows/fc-list.exe) [Title](TinyTeX/bin/windows/fc-match.exe) [Title](TinyTeX/bin/windows/fc-pattern.exe) [Title](TinyTeX/bin/windows/fc-query.exe) [Title](TinyTeX/bin/windows/fc-scan.exe) [Title](TinyTeX/bin/windows/fc-validate.exe) [Title](TinyTeX/bin/windows/fmtutil.exe) [Title](TinyTeX/bin/windows/fmtutil-sys.exe) [Title](TinyTeX/bin/windows/fmtutil-user.exe) [Title](TinyTeX/bin/windows/gftodvi.exe) [Title](TinyTeX/bin/windows/gftopk.exe) [Title](TinyTeX/bin/windows/gftype.exe) [Title](TinyTeX/bin/windows/hyperxmp-add-bytecount.exe) [Title](TinyTeX/bin/windows/inimf.exe) [Title](TinyTeX/bin/windows/initex.exe) [Title](TinyTeX/bin/windows/kpseaccess.exe) [Title](TinyTeX/bin/windows/kpsereadlink.exe) + +[Title](TinyTeX/bin/windows/icudt72.dll) [Title](TinyTeX/bin/windows/xetex.dll) + +latexindent.exe +[Title](TinyTeX/bin/windows/luahbtex.dll) +pdftex.dll +[Title](TinyTeX/bin/windows/teckit_compile.exe) +[Title](TinyTeX/bin/windows/dvipdfmx.dll) + +"C:\GIT\rendercv\rendercv\tinytex\vendor\TinyTeX\texmf-dist\source" +"C:\GIT\rendercv\rendercv\tinytex\vendor\TinyTeX\texmf-dist\fonts\vf" + +[Title](TinyTeX/texmf-dist/tex/latex/palatino) [Title](TinyTeX/texmf-dist/tex/latex/achemso) [Title](TinyTeX/texmf-dist/tex/latex/adjustbox) [Title](TinyTeX/texmf-dist/tex/latex/ae) [Title](TinyTeX/texmf-dist/tex/latex/algorithmicx) [Title](TinyTeX/texmf-dist/tex/latex/algorithms) [Title](TinyTeX/texmf-dist/tex/latex/amscls) [Title](TinyTeX/texmf-dist/tex/latex/amsfonts) [Title](TinyTeX/texmf-dist/tex/latex/amsmath) [Title](TinyTeX/texmf-dist/tex/latex/apacite) [Title](TinyTeX/texmf-dist/tex/latex/appendix) [Title](TinyTeX/texmf-dist/tex/latex/atveryend) [Title](TinyTeX/texmf-dist/tex/latex/auxhook) [Title](TinyTeX/texmf-dist/tex/latex/awesomebox) [Title](TinyTeX/texmf-dist/tex/latex/base) [Title](TinyTeX/texmf-dist/tex/latex/bbm-macros) [Title](TinyTeX/texmf-dist/tex/latex/beamer) [Title](TinyTeX/texmf-dist/tex/latex/biblatex) [Title](TinyTeX/texmf-dist/tex/latex/booktabs) [Title](TinyTeX/texmf-dist/tex/latex/caption) [Title](TinyTeX/texmf-dist/tex/latex/carlisle) [Title](TinyTeX/texmf-dist/tex/latex/catoptions) [Title](TinyTeX/texmf-dist/tex/latex/ccicons) [Title](TinyTeX/texmf-dist/tex/latex/changepage) [Title](TinyTeX/texmf-dist/tex/latex/chemgreek) [Title](TinyTeX/texmf-dist/tex/latex/cite) [Title](TinyTeX/texmf-dist/tex/latex/cleveref) [Title](TinyTeX/texmf-dist/tex/latex/collectbox) [Title](TinyTeX/texmf-dist/tex/latex/colortbl) [Title](TinyTeX/texmf-dist/tex/latex/comment) [Title](TinyTeX/texmf-dist/tex/latex/courier) [Title](TinyTeX/texmf-dist/tex/latex/crop) [Title](TinyTeX/texmf-dist/tex/latex/csquotes) [Title](TinyTeX/texmf-dist/tex/latex/currfile) [Title](TinyTeX/texmf-dist/tex/latex/dblfloatfix) [Title](TinyTeX/texmf-dist/tex/latex/doclicense) [Title](TinyTeX/texmf-dist/tex/latex/draftwatermark) [Title](TinyTeX/texmf-dist/tex/latex/eepic) [Title](TinyTeX/texmf-dist/tex/latex/endfloat) [Title](TinyTeX/texmf-dist/tex/latex/endnotes) [Title](TinyTeX/texmf-dist/tex/latex/enumitem) [Title](TinyTeX/texmf-dist/tex/latex/environ) [Title](TinyTeX/texmf-dist/tex/latex/epstopdf-pkg) [Title](TinyTeX/texmf-dist/tex/latex/eso-pic) [Title](TinyTeX/texmf-dist/tex/latex/esvect) [Title](TinyTeX/texmf-dist/tex/latex/etex-pkg) [Title](TinyTeX/texmf-dist/tex/latex/etoolbox) [Title](TinyTeX/texmf-dist/tex/latex/euenc) [Title](TinyTeX/texmf-dist/tex/latex/eurosym) [Title](TinyTeX/texmf-dist/tex/latex/everysel) [Title](TinyTeX/texmf-dist/tex/latex/everyshi) [Title](TinyTeX/texmf-dist/tex/latex/extsizes) [Title](TinyTeX/texmf-dist/tex/latex/fancyhdr) [Title](TinyTeX/texmf-dist/tex/latex/fancyvrb) [Title](TinyTeX/texmf-dist/tex/latex/filehook) [Title](TinyTeX/texmf-dist/tex/latex/filemod) [Title](TinyTeX/texmf-dist/tex/latex/firstaid) [Title](TinyTeX/texmf-dist/tex/latex/floatflt) [Title](TinyTeX/texmf-dist/tex/latex/floatrow) [Title](TinyTeX/texmf-dist/tex/latex/fmtcount) [Title](TinyTeX/texmf-dist/tex/latex/fontawesome5) [Title](TinyTeX/texmf-dist/tex/latex/fontaxes) [Title](TinyTeX/texmf-dist/tex/latex/footmisc) [Title](TinyTeX/texmf-dist/tex/latex/forarray) [Title](TinyTeX/texmf-dist/tex/latex/fp) [Title](TinyTeX/texmf-dist/tex/latex/framed) [Title](TinyTeX/texmf-dist/tex/latex/gincltex) [Title](TinyTeX/texmf-dist/tex/latex/graphics) [Title](TinyTeX/texmf-dist/tex/latex/graphics-cfg) [Title](TinyTeX/texmf-dist/tex/latex/graphics-def) [Title](TinyTeX/texmf-dist/tex/latex/grfext) [Title](TinyTeX/texmf-dist/tex/latex/grffile) [Title](TinyTeX/texmf-dist/tex/latex/hardwrap) [Title](TinyTeX/texmf-dist/tex/latex/helvetic) [Title](TinyTeX/texmf-dist/tex/latex/hycolor) [Title](TinyTeX/texmf-dist/tex/latex/hyphenat) [Title](TinyTeX/texmf-dist/tex/latex/ifmtarg) [Title](TinyTeX/texmf-dist/tex/latex/inconsolata) [Title](TinyTeX/texmf-dist/tex/latex/jknapltx) [Title](TinyTeX/texmf-dist/tex/latex/koma-script) [Title](TinyTeX/texmf-dist/tex/latex/kvoptions) [Title](TinyTeX/texmf-dist/tex/latex/kvsetkeys) [Title](TinyTeX/texmf-dist/tex/latex/l3backend) [Title](TinyTeX/texmf-dist/tex/latex/l3kernel) [Title](TinyTeX/texmf-dist/tex/latex/l3packages) [Title](TinyTeX/texmf-dist/tex/latex/lastpage) [Title](TinyTeX/texmf-dist/tex/latex/letltxmacro) [Title](TinyTeX/texmf-dist/tex/latex/lettrine) [Title](TinyTeX/texmf-dist/tex/latex/libertine) [Title](TinyTeX/texmf-dist/tex/latex/lineno) [Title](TinyTeX/texmf-dist/tex/latex/lipsum) [Title](TinyTeX/texmf-dist/tex/latex/listings) [Title](TinyTeX/texmf-dist/tex/latex/lm) [Title](TinyTeX/texmf-dist/tex/latex/logreq) [Title](TinyTeX/texmf-dist/tex/latex/ltxkeys) [Title](TinyTeX/texmf-dist/tex/latex/ly1) [Title](TinyTeX/texmf-dist/tex/latex/makecell) [Title](TinyTeX/texmf-dist/tex/latex/makecmds) [Title](TinyTeX/texmf-dist/tex/latex/marginnote) [Title](TinyTeX/texmf-dist/tex/latex/marvosym) [Title](TinyTeX/texmf-dist/tex/latex/mathalpha) [Title](TinyTeX/texmf-dist/tex/latex/mathtools) [Title](TinyTeX/texmf-dist/tex/latex/mdframed) [Title](TinyTeX/texmf-dist/tex/latex/mdwtools) [Title](TinyTeX/texmf-dist/tex/latex/metalogo) [Title](TinyTeX/texmf-dist/tex/latex/mhchem) [Title](TinyTeX/texmf-dist/tex/latex/microtype) [Title](TinyTeX/texmf-dist/tex/latex/mnras) [Title](TinyTeX/texmf-dist/tex/latex/morefloats) [Title](TinyTeX/texmf-dist/tex/latex/moreverb) [Title](TinyTeX/texmf-dist/tex/latex/ms) [Title](TinyTeX/texmf-dist/tex/latex/mweights) [Title](TinyTeX/texmf-dist/tex/latex/natbib) [Title](TinyTeX/texmf-dist/tex/latex/ncntrsbk) [Title](TinyTeX/texmf-dist/tex/latex/needspace) [Title](TinyTeX/texmf-dist/tex/latex/newfloat) [Title](TinyTeX/texmf-dist/tex/latex/newtx) [Title](TinyTeX/texmf-dist/tex/latex/ntgclass) [Title](TinyTeX/texmf-dist/tex/latex/oberdiek) + + +[Title](TinyTeX/texmf-dist/tex/latex/paralist) [Title](TinyTeX/texmf-dist/tex/latex/pbox) [Title](TinyTeX/texmf-dist/tex/latex/pdfcol) [Title](TinyTeX/texmf-dist/tex/latex/pdflscape) [Title](TinyTeX/texmf-dist/tex/latex/pdfpages) [Title](TinyTeX/texmf-dist/tex/latex/pdfsync) [Title](TinyTeX/texmf-dist/tex/latex/pgf) [Title](TinyTeX/texmf-dist/tex/latex/picinpar) [Title](TinyTeX/texmf-dist/tex/latex/placeins) [Title](TinyTeX/texmf-dist/tex/latex/polyglossia) [Title](TinyTeX/texmf-dist/tex/latex/preprint) [Title](TinyTeX/texmf-dist/tex/latex/preview) [Title](TinyTeX/texmf-dist/tex/latex/psfrag) [Title](TinyTeX/texmf-dist/tex/latex/psnfss) [Title](TinyTeX/texmf-dist/tex/latex/ragged2e) [Title](TinyTeX/texmf-dist/tex/latex/realscripts) [Title](TinyTeX/texmf-dist/tex/latex/refcount) [Title](TinyTeX/texmf-dist/tex/latex/rerunfilecheck) [Title](TinyTeX/texmf-dist/tex/latex/revtex4-1) [Title](TinyTeX/texmf-dist/tex/latex/roboto) [Title](TinyTeX/texmf-dist/tex/latex/sauerj) [Title](TinyTeX/texmf-dist/tex/latex/sectsty) [Title](TinyTeX/texmf-dist/tex/latex/seqsplit) [Title](TinyTeX/texmf-dist/tex/latex/setspace) [Title](TinyTeX/texmf-dist/tex/latex/sidecap) [Title](TinyTeX/texmf-dist/tex/latex/sidenotes) [Title](TinyTeX/texmf-dist/tex/latex/srcltx) [Title](TinyTeX/texmf-dist/tex/latex/standalone) [Title](TinyTeX/texmf-dist/tex/latex/stix) [Title](TinyTeX/texmf-dist/tex/latex/stmaryrd) [Title](TinyTeX/texmf-dist/tex/latex/sttools) [Title](TinyTeX/texmf-dist/tex/latex/subfig) [Title](TinyTeX/texmf-dist/tex/latex/subfigure) [Title](TinyTeX/texmf-dist/tex/latex/symbol) [Title](TinyTeX/texmf-dist/tex/latex/tabto-ltx) [Title](TinyTeX/texmf-dist/tex/latex/tabu) [Title](TinyTeX/texmf-dist/tex/latex/tcolorbox) [Title](TinyTeX/texmf-dist/tex/latex/tex-gyre) [Title](TinyTeX/texmf-dist/tex/latex/textcase) [Title](TinyTeX/texmf-dist/tex/latex/thmtools) [Title](TinyTeX/texmf-dist/tex/latex/threeparttable) [Title](TinyTeX/texmf-dist/tex/latex/threeparttablex) [Title](TinyTeX/texmf-dist/tex/latex/tikzfill) [Title](TinyTeX/texmf-dist/tex/latex/times) [Title](TinyTeX/texmf-dist/tex/latex/tipa) [Title](TinyTeX/texmf-dist/tex/latex/tools) [Title](TinyTeX/texmf-dist/tex/latex/totcount) [Title](TinyTeX/texmf-dist/tex/latex/totpages) [Title](TinyTeX/texmf-dist/tex/latex/translator) [Title](TinyTeX/texmf-dist/tex/latex/trimspaces) [Title](TinyTeX/texmf-dist/tex/latex/tufte-latex) [Title](TinyTeX/texmf-dist/tex/latex/ucs) [Title](TinyTeX/texmf-dist/tex/latex/unicode-math) [Title](TinyTeX/texmf-dist/tex/latex/units) [Title](TinyTeX/texmf-dist/tex/latex/upquote) [Title](TinyTeX/texmf-dist/tex/latex/url) [Title](TinyTeX/texmf-dist/tex/latex/varwidth) [Title](TinyTeX/texmf-dist/tex/latex/vmargin) [Title](TinyTeX/texmf-dist/tex/latex/vruler) [Title](TinyTeX/texmf-dist/tex/latex/wallpaper) [Title](TinyTeX/texmf-dist/tex/latex/wrapfig) [Title](TinyTeX/texmf-dist/tex/latex/xargs) [Title](TinyTeX/texmf-dist/tex/latex/xkeyval) [Title](TinyTeX/texmf-dist/tex/latex/xpatch) [Title](TinyTeX/texmf-dist/tex/latex/xwatermark) [Title](TinyTeX/texmf-dist/tex/latex/zapfchan) [Title](TinyTeX/texmf-dist/tex/latex/zapfding) [Title](TinyTeX/texmf-dist/tex/latex/zref) \ No newline at end of file