pymarktex
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# Built-in modules # 5import os, shutil, codecs, functools, importlib 6 7# Internal modules # 8from autopaths import Path 9from autopaths.tmp_path import new_temp_dir 10 11# Third party modules # 12 13############################################################################### 14class Document(object): 15 """The main object is the document to be generated from Markdown text.""" 16 17 # By default we don't rerun graphs # 18 rerun = False 19 20 # By default we don't print debug messages # 21 verbose = False 22 23 def __init__(self, input_path, output_path=None, builtin_template=None): 24 # Input # 25 self.input_path = Path(input_path) 26 # Output # 27 if output_path is None: 28 self.output_path = Path(self.default_output_name) 29 else: 30 self.output_path = Path(output_path) 31 32 def __bool__(self): 33 return self.output_path.exists 34 35 def __call__(self, *args, **kwargs): 36 return self.generate(*args, **kwargs) 37 38 def generate(self): 39 # Make a general temporary directory # 40 self.tmp_dir = new_temp_dir(prefix='pymrktx-') 41 # Main steps # 42 self.make_markdown() 43 self.make_body() 44 self.make_latex() 45 self.make_pdf() 46 # For convenience # 47 return self.output_path 48 49 @property 50 def default_options(self): 51 return { 52 'name': os.environ.get('USER_FULL_NAME'), 53 'status': os.environ.get('USER_STATUS'), 54 'company': os.environ.get('USER_COMPANY'), 55 'subcompany': os.environ.get('USER_SUBCOMPANY'), 56 'title': "Auto-generated report", 57 } 58 59 @property 60 def default_output_name(self): 61 return Path(os.path.splitext(self.input_path)[0] + '.pdf') 62 63 @property 64 def header_template(self): 65 from ..reports.template import Header 66 return Header 67 68 @property 69 def footer_template(self): 70 from ..reports.template import Footer 71 return Footer 72 73 def make_markdown(self): 74 # Make a temporary file # 75 self.markdown = Path(self.tmp_dir + 'main.md') 76 # Write the template to it # 77 self.markdown.write(str(self.template)) 78 79 def make_body(self): 80 """Convert the markdown body to a LaTeX body.""" 81 # Get the command line arguments # 82 cmd_args = {'read': 'markdown', 83 'write': 'latex', 84 '_return_cmd': True} 85 # Call pandoc executable # 86 import sh 87 result = sh.Command('pandoc')(self.markdown, **cmd_args) 88 # Convert to standard out to UTF-8 # 89 self.body = str(result.stdout, 'utf-8') 90 # Switch the verbatim environments to fancy verbatim # 91 self.body = self.body.replace('begin{verbatim}', 'begin{Verbatim}') 92 self.body = self.body.replace('end{verbatim}', 'end{Verbatim}') 93 94 def make_latex(self, params=None, header=None, footer=None): 95 """Add the LaTeX header and footer to the LaTeX body.""" 96 # Copy the options # 97 options = self.default_options.copy() 98 if params: options.update(params) 99 if hasattr(self, 'params'): options.update(self.params) 100 # Save the header and footer # 101 self.header = header 102 self.footer = footer 103 # Default values for the header and footer # 104 if header is None: self.header = self.header_template(options) 105 if footer is None: self.footer = self.footer_template() 106 # The LaTeX string # 107 self.latex = str(self.header) + self.body + str(self.footer) 108 109 def make_pdf(self, safe=False, include_src=False, verbose=False): 110 """Call xetex.""" 111 # Paths # 112 self.tmp_tex = Path(self.tmp_dir + 'main.tex') 113 self.tmp_stdout = Path(self.tmp_dir + 'stdout.txt') 114 self.tmp_stderr = Path(self.tmp_dir + 'stderr.txt') 115 self.tmp_log = Path(self.tmp_dir + 'main.log') 116 # Prepare the file # 117 with codecs.open(self.tmp_tex, 'w', encoding='utf8') as handle: 118 handle.write(self.latex) 119 # Add the command arguments # 120 self.cmd_params = ['--outdir', self.tmp_dir, self.tmp_tex] 121 # Optional message # 122 if verbose: print(self.cmd_params) 123 # Call twice for references # 124 self.call_tectonic(safe) 125 # Check output directory exists # 126 self.output_path.directory.create_if_not_exists() 127 # Move into place # 128 shutil.move(str(self.tmp_dir + 'main.pdf'), str(self.output_path)) 129 # Add the latex source file right next to the PDF file # 130 if include_src: 131 tex_path = self.output_path.replace_extension('tex') 132 tex_path.write(self.latex, encoding='utf-8') 133 # Remove the temporary directory # 134 self.tmp_dir.remove() 135 136 def call_tectonic(self, safe=False): 137 # The command # 138 import sh 139 cmd = sh.Command('tectonic') 140 # Test if it works # 141 try: 142 cmd(*self.cmd_params, 143 _ok_code = [0] if not safe else [0,1], 144 _err = str(self.tmp_stderr), 145 _out = str(self.tmp_stdout), 146 _return_cmd = True) 147 # In case we get a return code 1 # 148 except sh.ErrorReturnCode_1: 149 print('-' * 60) 150 print("tectonic exited with return code 1.") 151 if self.tmp_stdout.exists: 152 msg = "Here is the tail of the stdout at '%s':" 153 print(msg % self.tmp_stdout.unix_style) 154 print(self.tmp_stdout.pretty_tail) 155 if self.tmp_stderr.exists: 156 msg = "Here is the tail of the stderr at '%s':" 157 print(msg % self.tmp_stderr.unix_style) 158 print(self.tmp_stderr.pretty_tail) 159 if self.tmp_log.exists: 160 msg = "Here is the tail of the log at '%s':" 161 print(msg % self.tmp_log.unix_style) 162 print(self.tmp_log.pretty_tail) 163 print('-' * 60) 164 raise 165 166 def purge_cache(self): 167 """Some reports use pickled properties to avoid recalculations.""" 168 if not hasattr(self, 'cache_dir'): 169 raise Exception("No cache directory to purge.") 170 self.cache_dir.remove() 171 self.cache_dir.create() 172 173############################################################################### 174class MarkdownDocument(Document): 175 """ 176 Use this class when the markdown is already pre-computed or provided 177 by the user. 178 """ 179 180 def __init__(self, input_path, output_path=None, builtin_template=None): 181 # Call the parent class's __init__ method 182 super().__init__(input_path, output_path) 183 # Templates built-in # 184 self.builtin_template = builtin_template 185 if self.builtin_template: 186 subpackage = 'pymarktex.templates.' + builtin_template 187 subpackage = importlib.import_module(subpackage) 188 self.header_template = subpackage.HeaderTemplate 189 self.footer_template = subpackage.FooterTemplate 190 191 @functools.cached_property 192 def header_template(self): 193 import pymarktex.templates.sinclair_bio 194 return pymarktex.templates.sinclair_bio.HeaderTemplate 195 196 @functools.cached_property 197 def footer_template(self): 198 import pymarktex.templates.sinclair_bio 199 return pymarktex.templates.sinclair_bio.FooterTemplate 200 201 def make_markdown(self): 202 """Load file in memory and separate the options and body.""" 203 # Make a temporary file # 204 self.markdown = Path(self.tmp_dir + 'main.md') 205 # Write the template to it # 206 # Read the file # 207 self.input_path.must_exist() 208 self.input = self.input_path.contents_utf8 209 # Separate the top params and the rest of the markdown # 210 pattern = r'\A---(.+?)---(.+)' 211 import re 212 find_results = re.findall(pattern, self.input, re.M | re.DOTALL) 213 # We did not find any parameters # 214 if not find_results: 215 self.params = {} 216 self.markdown.write(self.input) 217 return 218 # We did find a set of parameters # 219 params, content = find_results[0] 220 # Split the parameters # 221 params = [i for i in params.strip().split('\n')] 222 # Strip prefixes # 223 params = [i.strip('* ') for i in params] 224 # Partition them into key/value # 225 params = [i.partition(':')[::2] for i in params] 226 # Transform into a dictionary # 227 self.params = dict([(k.strip(),v.strip()) for k,v in params]) 228 # Write only the part without the parameters # 229 self.markdown.write(content)
class
Document:
15class Document(object): 16 """The main object is the document to be generated from Markdown text.""" 17 18 # By default we don't rerun graphs # 19 rerun = False 20 21 # By default we don't print debug messages # 22 verbose = False 23 24 def __init__(self, input_path, output_path=None, builtin_template=None): 25 # Input # 26 self.input_path = Path(input_path) 27 # Output # 28 if output_path is None: 29 self.output_path = Path(self.default_output_name) 30 else: 31 self.output_path = Path(output_path) 32 33 def __bool__(self): 34 return self.output_path.exists 35 36 def __call__(self, *args, **kwargs): 37 return self.generate(*args, **kwargs) 38 39 def generate(self): 40 # Make a general temporary directory # 41 self.tmp_dir = new_temp_dir(prefix='pymrktx-') 42 # Main steps # 43 self.make_markdown() 44 self.make_body() 45 self.make_latex() 46 self.make_pdf() 47 # For convenience # 48 return self.output_path 49 50 @property 51 def default_options(self): 52 return { 53 'name': os.environ.get('USER_FULL_NAME'), 54 'status': os.environ.get('USER_STATUS'), 55 'company': os.environ.get('USER_COMPANY'), 56 'subcompany': os.environ.get('USER_SUBCOMPANY'), 57 'title': "Auto-generated report", 58 } 59 60 @property 61 def default_output_name(self): 62 return Path(os.path.splitext(self.input_path)[0] + '.pdf') 63 64 @property 65 def header_template(self): 66 from ..reports.template import Header 67 return Header 68 69 @property 70 def footer_template(self): 71 from ..reports.template import Footer 72 return Footer 73 74 def make_markdown(self): 75 # Make a temporary file # 76 self.markdown = Path(self.tmp_dir + 'main.md') 77 # Write the template to it # 78 self.markdown.write(str(self.template)) 79 80 def make_body(self): 81 """Convert the markdown body to a LaTeX body.""" 82 # Get the command line arguments # 83 cmd_args = {'read': 'markdown', 84 'write': 'latex', 85 '_return_cmd': True} 86 # Call pandoc executable # 87 import sh 88 result = sh.Command('pandoc')(self.markdown, **cmd_args) 89 # Convert to standard out to UTF-8 # 90 self.body = str(result.stdout, 'utf-8') 91 # Switch the verbatim environments to fancy verbatim # 92 self.body = self.body.replace('begin{verbatim}', 'begin{Verbatim}') 93 self.body = self.body.replace('end{verbatim}', 'end{Verbatim}') 94 95 def make_latex(self, params=None, header=None, footer=None): 96 """Add the LaTeX header and footer to the LaTeX body.""" 97 # Copy the options # 98 options = self.default_options.copy() 99 if params: options.update(params) 100 if hasattr(self, 'params'): options.update(self.params) 101 # Save the header and footer # 102 self.header = header 103 self.footer = footer 104 # Default values for the header and footer # 105 if header is None: self.header = self.header_template(options) 106 if footer is None: self.footer = self.footer_template() 107 # The LaTeX string # 108 self.latex = str(self.header) + self.body + str(self.footer) 109 110 def make_pdf(self, safe=False, include_src=False, verbose=False): 111 """Call xetex.""" 112 # Paths # 113 self.tmp_tex = Path(self.tmp_dir + 'main.tex') 114 self.tmp_stdout = Path(self.tmp_dir + 'stdout.txt') 115 self.tmp_stderr = Path(self.tmp_dir + 'stderr.txt') 116 self.tmp_log = Path(self.tmp_dir + 'main.log') 117 # Prepare the file # 118 with codecs.open(self.tmp_tex, 'w', encoding='utf8') as handle: 119 handle.write(self.latex) 120 # Add the command arguments # 121 self.cmd_params = ['--outdir', self.tmp_dir, self.tmp_tex] 122 # Optional message # 123 if verbose: print(self.cmd_params) 124 # Call twice for references # 125 self.call_tectonic(safe) 126 # Check output directory exists # 127 self.output_path.directory.create_if_not_exists() 128 # Move into place # 129 shutil.move(str(self.tmp_dir + 'main.pdf'), str(self.output_path)) 130 # Add the latex source file right next to the PDF file # 131 if include_src: 132 tex_path = self.output_path.replace_extension('tex') 133 tex_path.write(self.latex, encoding='utf-8') 134 # Remove the temporary directory # 135 self.tmp_dir.remove() 136 137 def call_tectonic(self, safe=False): 138 # The command # 139 import sh 140 cmd = sh.Command('tectonic') 141 # Test if it works # 142 try: 143 cmd(*self.cmd_params, 144 _ok_code = [0] if not safe else [0,1], 145 _err = str(self.tmp_stderr), 146 _out = str(self.tmp_stdout), 147 _return_cmd = True) 148 # In case we get a return code 1 # 149 except sh.ErrorReturnCode_1: 150 print('-' * 60) 151 print("tectonic exited with return code 1.") 152 if self.tmp_stdout.exists: 153 msg = "Here is the tail of the stdout at '%s':" 154 print(msg % self.tmp_stdout.unix_style) 155 print(self.tmp_stdout.pretty_tail) 156 if self.tmp_stderr.exists: 157 msg = "Here is the tail of the stderr at '%s':" 158 print(msg % self.tmp_stderr.unix_style) 159 print(self.tmp_stderr.pretty_tail) 160 if self.tmp_log.exists: 161 msg = "Here is the tail of the log at '%s':" 162 print(msg % self.tmp_log.unix_style) 163 print(self.tmp_log.pretty_tail) 164 print('-' * 60) 165 raise 166 167 def purge_cache(self): 168 """Some reports use pickled properties to avoid recalculations.""" 169 if not hasattr(self, 'cache_dir'): 170 raise Exception("No cache directory to purge.") 171 self.cache_dir.remove() 172 self.cache_dir.create()
The main object is the document to be generated from Markdown text.
def
make_body(self):
80 def make_body(self): 81 """Convert the markdown body to a LaTeX body.""" 82 # Get the command line arguments # 83 cmd_args = {'read': 'markdown', 84 'write': 'latex', 85 '_return_cmd': True} 86 # Call pandoc executable # 87 import sh 88 result = sh.Command('pandoc')(self.markdown, **cmd_args) 89 # Convert to standard out to UTF-8 # 90 self.body = str(result.stdout, 'utf-8') 91 # Switch the verbatim environments to fancy verbatim # 92 self.body = self.body.replace('begin{verbatim}', 'begin{Verbatim}') 93 self.body = self.body.replace('end{verbatim}', 'end{Verbatim}')
Convert the markdown body to a LaTeX body.
def
make_latex(self, params=None, header=None, footer=None):
95 def make_latex(self, params=None, header=None, footer=None): 96 """Add the LaTeX header and footer to the LaTeX body.""" 97 # Copy the options # 98 options = self.default_options.copy() 99 if params: options.update(params) 100 if hasattr(self, 'params'): options.update(self.params) 101 # Save the header and footer # 102 self.header = header 103 self.footer = footer 104 # Default values for the header and footer # 105 if header is None: self.header = self.header_template(options) 106 if footer is None: self.footer = self.footer_template() 107 # The LaTeX string # 108 self.latex = str(self.header) + self.body + str(self.footer)
Add the LaTeX header and footer to the LaTeX body.
def
make_pdf(self, safe=False, include_src=False, verbose=False):
110 def make_pdf(self, safe=False, include_src=False, verbose=False): 111 """Call xetex.""" 112 # Paths # 113 self.tmp_tex = Path(self.tmp_dir + 'main.tex') 114 self.tmp_stdout = Path(self.tmp_dir + 'stdout.txt') 115 self.tmp_stderr = Path(self.tmp_dir + 'stderr.txt') 116 self.tmp_log = Path(self.tmp_dir + 'main.log') 117 # Prepare the file # 118 with codecs.open(self.tmp_tex, 'w', encoding='utf8') as handle: 119 handle.write(self.latex) 120 # Add the command arguments # 121 self.cmd_params = ['--outdir', self.tmp_dir, self.tmp_tex] 122 # Optional message # 123 if verbose: print(self.cmd_params) 124 # Call twice for references # 125 self.call_tectonic(safe) 126 # Check output directory exists # 127 self.output_path.directory.create_if_not_exists() 128 # Move into place # 129 shutil.move(str(self.tmp_dir + 'main.pdf'), str(self.output_path)) 130 # Add the latex source file right next to the PDF file # 131 if include_src: 132 tex_path = self.output_path.replace_extension('tex') 133 tex_path.write(self.latex, encoding='utf-8') 134 # Remove the temporary directory # 135 self.tmp_dir.remove()
Call xetex.
def
call_tectonic(self, safe=False):
137 def call_tectonic(self, safe=False): 138 # The command # 139 import sh 140 cmd = sh.Command('tectonic') 141 # Test if it works # 142 try: 143 cmd(*self.cmd_params, 144 _ok_code = [0] if not safe else [0,1], 145 _err = str(self.tmp_stderr), 146 _out = str(self.tmp_stdout), 147 _return_cmd = True) 148 # In case we get a return code 1 # 149 except sh.ErrorReturnCode_1: 150 print('-' * 60) 151 print("tectonic exited with return code 1.") 152 if self.tmp_stdout.exists: 153 msg = "Here is the tail of the stdout at '%s':" 154 print(msg % self.tmp_stdout.unix_style) 155 print(self.tmp_stdout.pretty_tail) 156 if self.tmp_stderr.exists: 157 msg = "Here is the tail of the stderr at '%s':" 158 print(msg % self.tmp_stderr.unix_style) 159 print(self.tmp_stderr.pretty_tail) 160 if self.tmp_log.exists: 161 msg = "Here is the tail of the log at '%s':" 162 print(msg % self.tmp_log.unix_style) 163 print(self.tmp_log.pretty_tail) 164 print('-' * 60) 165 raise
def
purge_cache(self):
167 def purge_cache(self): 168 """Some reports use pickled properties to avoid recalculations.""" 169 if not hasattr(self, 'cache_dir'): 170 raise Exception("No cache directory to purge.") 171 self.cache_dir.remove() 172 self.cache_dir.create()
Some reports use pickled properties to avoid recalculations.
175class MarkdownDocument(Document): 176 """ 177 Use this class when the markdown is already pre-computed or provided 178 by the user. 179 """ 180 181 def __init__(self, input_path, output_path=None, builtin_template=None): 182 # Call the parent class's __init__ method 183 super().__init__(input_path, output_path) 184 # Templates built-in # 185 self.builtin_template = builtin_template 186 if self.builtin_template: 187 subpackage = 'pymarktex.templates.' + builtin_template 188 subpackage = importlib.import_module(subpackage) 189 self.header_template = subpackage.HeaderTemplate 190 self.footer_template = subpackage.FooterTemplate 191 192 @functools.cached_property 193 def header_template(self): 194 import pymarktex.templates.sinclair_bio 195 return pymarktex.templates.sinclair_bio.HeaderTemplate 196 197 @functools.cached_property 198 def footer_template(self): 199 import pymarktex.templates.sinclair_bio 200 return pymarktex.templates.sinclair_bio.FooterTemplate 201 202 def make_markdown(self): 203 """Load file in memory and separate the options and body.""" 204 # Make a temporary file # 205 self.markdown = Path(self.tmp_dir + 'main.md') 206 # Write the template to it # 207 # Read the file # 208 self.input_path.must_exist() 209 self.input = self.input_path.contents_utf8 210 # Separate the top params and the rest of the markdown # 211 pattern = r'\A---(.+?)---(.+)' 212 import re 213 find_results = re.findall(pattern, self.input, re.M | re.DOTALL) 214 # We did not find any parameters # 215 if not find_results: 216 self.params = {} 217 self.markdown.write(self.input) 218 return 219 # We did find a set of parameters # 220 params, content = find_results[0] 221 # Split the parameters # 222 params = [i for i in params.strip().split('\n')] 223 # Strip prefixes # 224 params = [i.strip('* ') for i in params] 225 # Partition them into key/value # 226 params = [i.partition(':')[::2] for i in params] 227 # Transform into a dictionary # 228 self.params = dict([(k.strip(),v.strip()) for k,v in params]) 229 # Write only the part without the parameters # 230 self.markdown.write(content)
Use this class when the markdown is already pre-computed or provided by the user.
MarkdownDocument(input_path, output_path=None, builtin_template=None)
181 def __init__(self, input_path, output_path=None, builtin_template=None): 182 # Call the parent class's __init__ method 183 super().__init__(input_path, output_path) 184 # Templates built-in # 185 self.builtin_template = builtin_template 186 if self.builtin_template: 187 subpackage = 'pymarktex.templates.' + builtin_template 188 subpackage = importlib.import_module(subpackage) 189 self.header_template = subpackage.HeaderTemplate 190 self.footer_template = subpackage.FooterTemplate
def
make_markdown(self):
202 def make_markdown(self): 203 """Load file in memory and separate the options and body.""" 204 # Make a temporary file # 205 self.markdown = Path(self.tmp_dir + 'main.md') 206 # Write the template to it # 207 # Read the file # 208 self.input_path.must_exist() 209 self.input = self.input_path.contents_utf8 210 # Separate the top params and the rest of the markdown # 211 pattern = r'\A---(.+?)---(.+)' 212 import re 213 find_results = re.findall(pattern, self.input, re.M | re.DOTALL) 214 # We did not find any parameters # 215 if not find_results: 216 self.params = {} 217 self.markdown.write(self.input) 218 return 219 # We did find a set of parameters # 220 params, content = find_results[0] 221 # Split the parameters # 222 params = [i for i in params.strip().split('\n')] 223 # Strip prefixes # 224 params = [i.strip('* ') for i in params] 225 # Partition them into key/value # 226 params = [i.partition(':')[::2] for i in params] 227 # Transform into a dictionary # 228 self.params = dict([(k.strip(),v.strip()) for k,v in params]) 229 # Write only the part without the parameters # 230 self.markdown.write(content)
Load file in memory and separate the options and body.