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.

Document(input_path, output_path=None, builtin_template=None)
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)
rerun = False
verbose = False
input_path
def generate(self):
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
default_options
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        }
default_output_name
60    @property
61    def default_output_name(self):
62        return Path(os.path.splitext(self.input_path)[0] + '.pdf')
header_template
64    @property
65    def header_template(self):
66        from ..reports.template import Header
67        return Header
footer_template
69    @property
70    def footer_template(self):
71        from ..reports.template import Footer
72        return Footer
def make_markdown(self):
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))
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.

class MarkdownDocument(Document):
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
builtin_template
header_template
192    @functools.cached_property
193    def header_template(self):
194        import pymarktex.templates.sinclair_bio
195        return pymarktex.templates.sinclair_bio.HeaderTemplate
footer_template
197    @functools.cached_property
198    def footer_template(self):
199        import pymarktex.templates.sinclair_bio
200        return pymarktex.templates.sinclair_bio.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.