mirror of https://github.com/eyhc1/rendercv.git
add markdown rendering capability
This commit is contained in:
parent
56e1c76c2e
commit
4ab6c49f63
|
@ -19,13 +19,16 @@ from datetime import date as Date
|
|||
from typing import Optional, Literal, Any
|
||||
|
||||
import jinja2
|
||||
import markdown
|
||||
import fpdf
|
||||
|
||||
from . import data_models as dm
|
||||
|
||||
|
||||
class LaTeXFile:
|
||||
"""This class represents a $\\LaTeX$ file. It generates the $\\LaTeX$ code with the
|
||||
data model and Jinja2 templates.
|
||||
class TemplatedFile:
|
||||
"""This class is a base class for LaTeXFile and MarkdownFile classes. It contains
|
||||
the common methods and attributes for both classes. These classes are used to
|
||||
generate the LaTeX and Markdown files with the data model and Jinja2 templates.
|
||||
|
||||
Args:
|
||||
data_model (dm.RenderCVDataModel): The data model.
|
||||
|
@ -41,33 +44,9 @@ class LaTeXFile:
|
|||
self.design = data_model.design
|
||||
self.environment = environment
|
||||
|
||||
# Template the preamble, header, and sections:
|
||||
self.preamble = self.template("Preamble")
|
||||
self.header = self.template("Header")
|
||||
self.sections = []
|
||||
for section in self.cv.sections:
|
||||
section_beginning = self.template(
|
||||
"SectionBeginning", section_title=section.title
|
||||
)
|
||||
entries = []
|
||||
for i, entry in enumerate(section.entries):
|
||||
if i == 0:
|
||||
is_first_entry = True
|
||||
else:
|
||||
is_first_entry = False
|
||||
entries.append(
|
||||
self.template(
|
||||
section.entry_type,
|
||||
entry=entry,
|
||||
section_title=section.title,
|
||||
is_first_entry=is_first_entry,
|
||||
)
|
||||
)
|
||||
section_ending = self.template("SectionEnding", section_title=section.title)
|
||||
self.sections.append((section_beginning, entries, section_ending))
|
||||
|
||||
def template(
|
||||
self,
|
||||
theme_name,
|
||||
template_name: Literal[
|
||||
"EducationEntry",
|
||||
"ExperienceEntry",
|
||||
|
@ -80,6 +59,7 @@ class LaTeXFile:
|
|||
"SectionBeginning",
|
||||
"SectionEnding",
|
||||
],
|
||||
extension: str,
|
||||
entry: Optional[
|
||||
dm.EducationEntry
|
||||
| dm.ExperienceEntry
|
||||
|
@ -108,10 +88,10 @@ class LaTeXFile:
|
|||
section.
|
||||
|
||||
Returns:
|
||||
str: The templated $\\LaTeX$ code.
|
||||
str: The templated file.
|
||||
"""
|
||||
template = self.environment.get_template(
|
||||
f"{self.design.theme}/{template_name}.j2.tex"
|
||||
f"{theme_name}/{template_name}.j2.{extension}"
|
||||
)
|
||||
|
||||
# Loop through the entry attributes and make them "" if they are None:
|
||||
|
@ -129,7 +109,7 @@ class LaTeXFile:
|
|||
entry.__setattr__(key, "")
|
||||
|
||||
# The arguments of the template can be used in the template file:
|
||||
latex_code = template.render(
|
||||
result = template.render(
|
||||
cv=self.cv,
|
||||
design=self.design,
|
||||
entry=entry,
|
||||
|
@ -138,23 +118,200 @@ class LaTeXFile:
|
|||
is_first_entry=is_first_entry,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def get_full_code(self, main_template_name: str, **kwargs) -> str:
|
||||
"""Combine all the templates to get the full code of the file."""
|
||||
main_template = self.environment.get_template(main_template_name)
|
||||
latex_code = main_template.render(
|
||||
**kwargs,
|
||||
)
|
||||
return latex_code
|
||||
|
||||
|
||||
class LaTeXFile(TemplatedFile):
|
||||
"""This class represents a $\\LaTeX$ file. It generates the $\\LaTeX$ code with the
|
||||
data model and Jinja2 templates. It inherits from the TemplatedFile class.
|
||||
"""
|
||||
|
||||
def render_templates(self):
|
||||
"""Render and return all the templates for the $\\LaTeX$ file.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str, List[Tuple[str, List[str], str]]]: The preamble, header, and
|
||||
sections of the $\\LaTeX$ file.
|
||||
"""
|
||||
# Template the preamble, header, and sections:
|
||||
preamble = self.template("Preamble")
|
||||
header = self.template("Header")
|
||||
sections = []
|
||||
for section in self.cv.sections:
|
||||
section_beginning = self.template(
|
||||
"SectionBeginning", section_title=section.title
|
||||
)
|
||||
entries = []
|
||||
for i, entry in enumerate(section.entries):
|
||||
if i == 0:
|
||||
is_first_entry = True
|
||||
else:
|
||||
is_first_entry = False
|
||||
entries.append(
|
||||
self.template(
|
||||
section.entry_type,
|
||||
entry=entry,
|
||||
section_title=section.title,
|
||||
is_first_entry=is_first_entry,
|
||||
)
|
||||
)
|
||||
section_ending = self.template("SectionEnding", section_title=section.title)
|
||||
sections.append((section_beginning, entries, section_ending))
|
||||
|
||||
return preamble, header, sections
|
||||
|
||||
def template(
|
||||
self,
|
||||
template_name: Literal[
|
||||
"EducationEntry",
|
||||
"ExperienceEntry",
|
||||
"NormalEntry",
|
||||
"PublicationEntry",
|
||||
"OneLineEntry",
|
||||
"TextEntry",
|
||||
"Header",
|
||||
"Preamble",
|
||||
"SectionBeginning",
|
||||
"SectionEnding",
|
||||
],
|
||||
entry: Optional[
|
||||
dm.EducationEntry
|
||||
| dm.ExperienceEntry
|
||||
| dm.NormalEntry
|
||||
| dm.PublicationEntry
|
||||
| dm.OneLineEntry
|
||||
| str # TextEntry
|
||||
] = None,
|
||||
section_title: Optional[str] = None,
|
||||
is_first_entry: Optional[bool] = None,
|
||||
) -> str:
|
||||
"""Template one of the files in the `themes` directory."""
|
||||
result = super().template(
|
||||
self.design.theme,
|
||||
template_name,
|
||||
"tex",
|
||||
entry,
|
||||
section_title,
|
||||
is_first_entry,
|
||||
)
|
||||
return result
|
||||
|
||||
def get_latex_code(self):
|
||||
"""Get the $\\LaTeX$ code of the file."""
|
||||
main_template = self.environment.get_template("main.j2.tex")
|
||||
latex_code = main_template.render(
|
||||
header=self.header,
|
||||
preamble=self.preamble,
|
||||
sections=self.sections,
|
||||
preamble, header, sections = self.render_templates()
|
||||
return self.get_full_code(
|
||||
"main.j2.tex",
|
||||
preamble=preamble,
|
||||
header=header,
|
||||
sections=sections,
|
||||
)
|
||||
return latex_code
|
||||
|
||||
def generate_latex_file(self, file_path: pathlib.Path):
|
||||
"""Write the $\\LaTeX$ code to a file."""
|
||||
file_path.write_text(self.get_latex_code(), encoding="utf-8")
|
||||
|
||||
|
||||
class MarkdownFile(TemplatedFile):
|
||||
"""This class represents a Markdown file. It generates the Markdown code with the
|
||||
data model and Jinja2 templates. It inherits from the TemplatedFile class. Markdown
|
||||
files are generated to produce a PDF which can be copy-pasted to
|
||||
[Grammarly](https://app.grammarly.com/) for proofreading.
|
||||
|
||||
Args:
|
||||
data_model (dm.RenderCVDataModel): The data model.
|
||||
environment (jinja2.Environment): The Jinja2 environment.
|
||||
"""
|
||||
|
||||
def render_templates(self):
|
||||
"""Render and return all the templates for the Markdown file.
|
||||
|
||||
Returns:
|
||||
Tuple[str, List[Tuple[str, List[str]]]: The header and sections of the
|
||||
Markdown file.
|
||||
"""
|
||||
# Template the header and sections:
|
||||
header = self.template("Header")
|
||||
sections = []
|
||||
for section in self.cv.sections:
|
||||
section_beginning = self.template(
|
||||
"SectionBeginning", section_title=section.title
|
||||
)
|
||||
entries = []
|
||||
for i, entry in enumerate(section.entries):
|
||||
if i == 0:
|
||||
is_first_entry = True
|
||||
else:
|
||||
is_first_entry = False
|
||||
entries.append(
|
||||
self.template(
|
||||
section.entry_type,
|
||||
entry=entry,
|
||||
section_title=section.title,
|
||||
is_first_entry=is_first_entry,
|
||||
)
|
||||
)
|
||||
sections.append((section_beginning, entries))
|
||||
|
||||
return header, sections
|
||||
|
||||
def template(
|
||||
self,
|
||||
template_name: Literal[
|
||||
"EducationEntry",
|
||||
"ExperienceEntry",
|
||||
"NormalEntry",
|
||||
"PublicationEntry",
|
||||
"OneLineEntry",
|
||||
"TextEntry",
|
||||
"Header",
|
||||
"Preamble",
|
||||
"SectionBeginning",
|
||||
"SectionEnding",
|
||||
],
|
||||
entry: Optional[
|
||||
dm.EducationEntry
|
||||
| dm.ExperienceEntry
|
||||
| dm.NormalEntry
|
||||
| dm.PublicationEntry
|
||||
| dm.OneLineEntry
|
||||
| str # TextEntry
|
||||
] = None,
|
||||
section_title: Optional[str] = None,
|
||||
is_first_entry: Optional[bool] = None,
|
||||
) -> str:
|
||||
"""Template one of the files in the `themes` directory."""
|
||||
result = super().template(
|
||||
"markdown",
|
||||
template_name,
|
||||
"md",
|
||||
entry,
|
||||
section_title,
|
||||
is_first_entry,
|
||||
)
|
||||
return result
|
||||
|
||||
def get_markdown_code(self):
|
||||
"""Get the Markdown code of the file."""
|
||||
header, sections = self.render_templates()
|
||||
return self.get_full_code(
|
||||
"main.j2.md",
|
||||
header=header,
|
||||
sections=sections,
|
||||
)
|
||||
|
||||
def generate_markdown_file(self, file_path: pathlib.Path):
|
||||
"""Write the Markdown code to a file."""
|
||||
file_path.write_text(self.get_markdown_code(), encoding="utf-8")
|
||||
|
||||
|
||||
def make_matched_part_something(
|
||||
value: str, something: str, match_str: Optional[str] = None
|
||||
) -> str:
|
||||
|
@ -447,6 +604,35 @@ def generate_latex_file(
|
|||
return latex_file_path
|
||||
|
||||
|
||||
def generate_markdown_file(
|
||||
rendercv_data_model: dm.RenderCVDataModel, output_directory: pathlib.Path
|
||||
) -> pathlib.Path:
|
||||
"""Generate the Markdown file with the given data model and write it to the output
|
||||
directory.
|
||||
|
||||
Args:
|
||||
rendercv_data_model (dm.RenderCVDataModel): The data model.
|
||||
output_directory (pathlib.Path): Path to the output directory.
|
||||
Returns:
|
||||
pathlib.Path: The path to the generated Markdown file.
|
||||
"""
|
||||
# create output directory if it doesn't exist:
|
||||
if not output_directory.is_dir():
|
||||
output_directory.mkdir(parents=True)
|
||||
|
||||
jinja2_environment = setup_jinja2_environment()
|
||||
markdown_file_object = MarkdownFile(
|
||||
rendercv_data_model,
|
||||
jinja2_environment,
|
||||
)
|
||||
|
||||
markdown_file_name = f"{rendercv_data_model.cv.name.replace(' ', '_')}_CV.md"
|
||||
markdown_file_path = output_directory / markdown_file_name
|
||||
markdown_file_object.generate_markdown_file(markdown_file_path)
|
||||
|
||||
return markdown_file_path
|
||||
|
||||
|
||||
def copy_theme_files_to_output_directory(
|
||||
theme_name: str, output_directory: pathlib.Path
|
||||
):
|
||||
|
@ -512,9 +698,9 @@ def latex_to_pdf(latex_file_path: pathlib.Path) -> pathlib.Path:
|
|||
)
|
||||
|
||||
executables = {
|
||||
"win32": tinytex_binaries_directory / "windows" / "latexmk.exe",
|
||||
"linux": tinytex_binaries_directory / "x86_64-linux" / "latexmk",
|
||||
"darwin": tinytex_binaries_directory / "universal-darwin" / "latexmk",
|
||||
"win32": tinytex_binaries_directory / "windows" / "pdflatex.exe",
|
||||
"linux": tinytex_binaries_directory / "x86_64-linux" / "pdflatex",
|
||||
"darwin": tinytex_binaries_directory / "universal-darwin" / "pdflatex",
|
||||
}
|
||||
|
||||
if sys.platform not in executables:
|
||||
|
@ -524,16 +710,15 @@ def latex_to_pdf(latex_file_path: pathlib.Path) -> pathlib.Path:
|
|||
command = [
|
||||
executables[sys.platform],
|
||||
str(latex_file_path.absolute()),
|
||||
"-lualatex",
|
||||
]
|
||||
with subprocess.Popen(
|
||||
command,
|
||||
cwd=latex_file_path.parent,
|
||||
stdout=subprocess.DEVNULL, # don't capture the output
|
||||
stdout=subprocess.PIPE, # capture the output
|
||||
stderr=subprocess.DEVNULL, # don't capture the error
|
||||
stdin=subprocess.DEVNULL, # don't allow TinyTeX to ask for user input
|
||||
) as latex_process:
|
||||
latex_process.communicate() # wait for the process to finish
|
||||
output = latex_process.communicate() # wait for the process to finish
|
||||
if latex_process.returncode != 0:
|
||||
raise RuntimeError(
|
||||
"Running TinyTeX has failed! For debugging, we suggest running the"
|
||||
|
@ -542,10 +727,12 @@ def latex_to_pdf(latex_file_path: pathlib.Path) -> pathlib.Path:
|
|||
" ".join([str(command_part) for command_part in command]),
|
||||
"If you can't solve the problem, please open an issue on GitHub.",
|
||||
)
|
||||
|
||||
# clean the auxiliary files:
|
||||
else:
|
||||
output = output[0].decode("utf-8")
|
||||
if "Rerun to get" in output:
|
||||
# Run TinyTeX again to get the references right:
|
||||
subprocess.run(
|
||||
command + ["-c"],
|
||||
command,
|
||||
cwd=latex_file_path.parent,
|
||||
stdout=subprocess.DEVNULL, # don't capture the output
|
||||
stderr=subprocess.DEVNULL, # don't capture the error
|
||||
|
@ -561,3 +748,46 @@ def latex_to_pdf(latex_file_path: pathlib.Path) -> pathlib.Path:
|
|||
)
|
||||
|
||||
return pdf_file_path
|
||||
|
||||
|
||||
def markdown_to_html(markdown_file_path: pathlib.Path) -> pathlib.Path:
|
||||
"""C
|
||||
Args:
|
||||
markdown_file_path (pathlib.Path): The path to the Markdown file to convert.
|
||||
Returns:
|
||||
pathlib.Path: The path to the generated PDF file.
|
||||
"""
|
||||
|
||||
# check if the file exists:
|
||||
if not markdown_file_path.is_file():
|
||||
raise FileNotFoundError(f"The file {markdown_file_path} doesn't exist!")
|
||||
|
||||
pdf_file_path = markdown_file_path.with_suffix(".pdf")
|
||||
|
||||
# Convert the markdown file to HTML:
|
||||
html = markdown.markdown(markdown_file_path.read_text(encoding="utf-8"))
|
||||
|
||||
# write html into a file:
|
||||
html_file_path = markdown_file_path.with_suffix(".html")
|
||||
html_file_path.write_text(html, encoding="utf-8")
|
||||
|
||||
# Convert the HTML to PDF:
|
||||
# classic_theme_fonts_path = (
|
||||
# pathlib.Path(__file__).parent / "themes" / "classic" / "fonts"
|
||||
# )
|
||||
# regular_font_path = classic_theme_fonts_path / "SourceSans3-Regular.ttf"
|
||||
# bold_font_path = classic_theme_fonts_path / "SourceSans3-Bold.ttf"
|
||||
# italic_font_path = classic_theme_fonts_path / "SourceSans3-Italic.ttf"
|
||||
# bold_italic_font_path = classic_theme_fonts_path / "SourceSans3-BoldItalic.ttf"
|
||||
# pdf = fpdf.FPDF()
|
||||
# pdf.add_page()
|
||||
# pdf.add_font("SourceSans3", "", regular_font_path)
|
||||
# pdf.add_font("SourceSans3", "B", bold_font_path)
|
||||
# pdf.add_font("SourceSans3", "I", italic_font_path)
|
||||
# pdf.add_font("SourceSans3", "BI", bold_italic_font_path) # type: ignore
|
||||
# pdf.set_font("SourceSans3", size=10)
|
||||
# pdf.write_html(html)
|
||||
# os.chdir(markdown_file_path.parent)
|
||||
# pdf.output(pdf_file_path.name)
|
||||
|
||||
return pdf_file_path
|
||||
|
|
Loading…
Reference in New Issue