markdown_convert 1.2.14__tar.gz → 1.2.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown_convert
3
- Version: 1.2.14
3
+ Version: 1.2.16
4
4
  Summary: Convert Markdown files to PDF from your command line.
5
5
  Project-URL: homepage, https://github.com/Julynx/markdown_convert
6
6
  Author-email: Julio Cabria <juliocabria@tutanota.com>
@@ -13,8 +13,11 @@ from .modules.constants import RED, OPTIONS, OPTIONS_MODES
13
13
  from .modules.convert import convert, live_convert
14
14
  from .modules.resources import get_css_path, get_output_path, get_usage
15
15
  from .modules.utils import color
16
- from .modules.validate import (validate_css_path, validate_markdown_path,
17
- validate_output_path)
16
+ from .modules.validate import (
17
+ validate_css_path,
18
+ validate_markdown_path,
19
+ validate_output_path,
20
+ )
18
21
 
19
22
 
20
23
  def main():
@@ -32,11 +35,9 @@ def main():
32
35
  md_path = arg["markdown_file_path"]
33
36
  validate_markdown_path(md_path)
34
37
  except KeyError as key_err:
35
- raise IndexError("Missing 'markdown_file_path' argument.") \
36
- from key_err
38
+ raise IndexError("Missing 'markdown_file_path' argument.") from key_err
37
39
  except Exception as exc:
38
- raise IndexError(f"Invalid 'markdown_file_path' argument: {exc}") \
39
- from exc
40
+ raise IndexError(f"Invalid 'markdown_file_path' argument: {exc}") from exc
40
41
 
41
42
  # Get the mode
42
43
  try:
@@ -53,8 +54,7 @@ def main():
53
54
  except KeyError:
54
55
  css_path = get_css_path()
55
56
  except Exception as exc:
56
- raise IndexError(f"Invalid 'css_file_path' argument: {exc}") \
57
- from exc
57
+ raise IndexError(f"Invalid 'css_file_path' argument: {exc}") from exc
58
58
 
59
59
  # Get the output path
60
60
  output_path = None
@@ -65,11 +65,10 @@ def main():
65
65
  except KeyError:
66
66
  output_path = get_output_path(md_path, None)
67
67
  except Exception as exc:
68
- raise IndexError(f"Invalid 'output_path' argument: {exc}") \
69
- from exc
68
+ raise IndexError(f"Invalid 'output_path' argument: {exc}") from exc
70
69
 
71
70
  # Compile the markdown file
72
- print(f'\nGenerating PDF file from \'{md_path}\'...\n')
71
+ print(f"\nGenerating PDF file from '{md_path}'...\n")
73
72
  if mode == "once":
74
73
  convert(md_path, css_path, output_path)
75
74
  else:
@@ -77,11 +76,11 @@ def main():
77
76
 
78
77
  sys_exit(0)
79
78
 
79
+ # pylint: disable=W0718
80
80
  except Exception as err:
81
81
 
82
82
  asked_for_help = "--help" in arg or "-h" in arg
83
- show_usage = (isinstance(err, (IndexError, ValueError))
84
- or asked_for_help)
83
+ show_usage = isinstance(err, (IndexError, ValueError)) or asked_for_help
85
84
 
86
85
  if show_usage:
87
86
  print(get_usage())
@@ -92,5 +91,5 @@ def main():
92
91
  sys_exit(1)
93
92
 
94
93
 
95
- if __name__ == '__main__':
94
+ if __name__ == "__main__":
96
95
  main()
@@ -0,0 +1,4 @@
1
+ """
2
+ Empty file to make the folder a package.
3
+ Author: @julynx
4
+ """
@@ -0,0 +1,22 @@
1
+ """
2
+ This module contains the constants used in the markdown_convert package.
3
+ Author: @julynx
4
+ """
5
+
6
+ RED = "31"
7
+ GREEN = "32"
8
+ YELLOW = "33"
9
+ BLUE = "34"
10
+ MAGENTA = "35"
11
+ CYAN = "36"
12
+
13
+ OPTIONS = ("markdown_file_path", "--mode", "--css", "--out", "-h", "--help")
14
+
15
+ OPTIONS_MODES = ("once", "live")
16
+
17
+ MD_EXTENSIONS = {
18
+ "fenced-code-blocks": None,
19
+ "header-ids": None,
20
+ "breaks": {"on_newline": True},
21
+ "tables": None,
22
+ }
@@ -0,0 +1,247 @@
1
+ """
2
+ Module to convert a markdown file to a pdf file.
3
+ Author: @julynx
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import time
9
+ import warnings
10
+ from contextlib import redirect_stderr, redirect_stdout
11
+ from datetime import datetime
12
+ from io import StringIO
13
+ from pathlib import Path
14
+
15
+ import markdown2
16
+ import weasyprint
17
+
18
+ from .resources import get_css_path, get_code_css_path, get_output_path
19
+ from .utils import drop_duplicates
20
+ from .constants import MD_EXTENSIONS
21
+
22
+
23
+ def _suppress_warnings():
24
+ """
25
+ Suppress all warnings in production while preserving critical error handling.
26
+ Only errors and exceptions will be shown.
27
+ """
28
+ # Suppress all warnings but keep errors
29
+ warnings.filterwarnings("ignore", category=UserWarning)
30
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
31
+ warnings.filterwarnings("ignore", category=FutureWarning)
32
+ warnings.filterwarnings("ignore", category=PendingDeprecationWarning)
33
+ warnings.filterwarnings("ignore", category=ImportWarning)
34
+ warnings.filterwarnings("ignore", category=ResourceWarning)
35
+
36
+
37
+ def _silent_pdf_generation(func, *args, **kwargs):
38
+ """
39
+ Execute PDF generation function while suppressing all non-critical output.
40
+ Preserves exceptions and critical errors.
41
+ """
42
+ _suppress_warnings()
43
+
44
+ # Capture stdout and stderr to filter out warnings
45
+ stdout_capture = StringIO()
46
+ stderr_capture = StringIO()
47
+
48
+ try:
49
+ with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
50
+ result = func(*args, **kwargs)
51
+
52
+ # Check if there were any critical errors in stderr
53
+ stderr_content = stderr_capture.getvalue()
54
+ if stderr_content and any(
55
+ keyword in stderr_content.lower()
56
+ for keyword in ["error", "exception", "traceback", "failed"]
57
+ ):
58
+ # Print only critical errors, not warnings
59
+ print(stderr_content, file=sys.stderr)
60
+
61
+ return result
62
+
63
+ except Exception as exc:
64
+ # Always re-raise actual exceptions
65
+ raise exc
66
+
67
+
68
+ def convert(md_path, css_path=None, output_path=None, *, extend_default_css=True):
69
+ """
70
+ Convert a markdown file to a pdf file.
71
+
72
+ Args:
73
+ md_path (str): Path to the markdown file.
74
+ css_path (str=None): Path to the CSS file.
75
+ output_path (str=None): Path to the output file.
76
+ extend_default_css (bool=True): Extend the default CSS file.
77
+ """
78
+ if css_path is None:
79
+ css_path = get_css_path()
80
+
81
+ if output_path is None:
82
+ output_path = get_output_path(md_path, None)
83
+
84
+ if extend_default_css:
85
+ css_sources = [get_code_css_path(), get_css_path(), css_path]
86
+ else:
87
+ css_sources = [get_code_css_path(), css_path]
88
+
89
+ css_sources = drop_duplicates(css_sources)
90
+
91
+ try:
92
+ html = markdown2.markdown_path(md_path, extras=MD_EXTENSIONS)
93
+
94
+ # Use silent PDF generation to suppress warnings
95
+ _silent_pdf_generation(
96
+ lambda: weasyprint.HTML(string=html, base_url=".").write_pdf(
97
+ target=output_path, stylesheets=list(css_sources)
98
+ )
99
+ )
100
+
101
+ except Exception as exc:
102
+ raise RuntimeError(exc) from exc
103
+
104
+
105
+ def live_convert(md_path, css_path=None, output_path=None, *, extend_default_css=True):
106
+ """
107
+ Convert a markdown file to a pdf file and watch for changes.
108
+
109
+ Args:
110
+ md_path (str): Path to the markdown file.
111
+ css_path (str=None): Path to the CSS file.
112
+ output_path (str=None): Path to the output file.
113
+ extend_default_css (bool=True): Extend the default CSS file.
114
+ """
115
+ if css_path is None:
116
+ css_path = get_css_path()
117
+
118
+ if output_path is None:
119
+ output_path = get_output_path(md_path, None)
120
+
121
+ live_converter = LiveConverter(
122
+ md_path, css_path, output_path, extend_default_css=extend_default_css, loud=True
123
+ )
124
+ live_converter.observe()
125
+
126
+
127
+ def convert_text(md_text, css_text=None, *, extend_default_css=True):
128
+ """
129
+ Convert markdown text to a pdf file.
130
+
131
+ Args:
132
+ md_text (str): Markdown text.
133
+ css_text (str=None): CSS text.
134
+ extend_default_css (bool=True): Extend the default CSS file.
135
+
136
+ Returns:
137
+ PDF file as bytes.
138
+ """
139
+ default_css = Path(get_css_path()).read_text(encoding="utf-8")
140
+ code_css = Path(get_code_css_path()).read_text(encoding="utf-8")
141
+
142
+ if css_text is None:
143
+ css_text = default_css
144
+
145
+ if extend_default_css:
146
+ css_sources = [code_css, default_css, css_text]
147
+ else:
148
+ css_sources = [code_css, css_text]
149
+
150
+ css_sources = [weasyprint.CSS(string=css) for css in drop_duplicates(css_sources)]
151
+
152
+ try:
153
+ html = markdown2.markdown(md_text, extras=MD_EXTENSIONS)
154
+
155
+ # Use silent PDF generation to suppress warnings
156
+ return _silent_pdf_generation(
157
+ lambda: weasyprint.HTML(string=html, base_url=".").write_pdf(
158
+ stylesheets=css_sources
159
+ )
160
+ )
161
+
162
+ except Exception as exc:
163
+ raise RuntimeError(exc) from exc
164
+
165
+
166
+ class LiveConverter:
167
+ """
168
+ Class to convert a markdown file to a pdf file and watch for changes.
169
+ """
170
+
171
+ def __init__(
172
+ self, md_path, css_path, output_path, *, extend_default_css=True, loud=False
173
+ ):
174
+ """
175
+ Initialize the LiveConverter class.
176
+
177
+ Args:
178
+ md_path (str): Path to the markdown file.
179
+ css_path (str): Path to the CSS file.
180
+ output_path (str): Path to the output file.
181
+ extend_default_css (bool): Extend the default CSS file.
182
+ """
183
+ self.md_path = Path(md_path).absolute()
184
+ self.css_path = Path(css_path).absolute()
185
+ self.output_path = output_path
186
+ self.extend_default_css = extend_default_css
187
+ self.loud = loud
188
+
189
+ self.md_last_modified = None
190
+ self.css_last_modified = None
191
+
192
+ def get_last_modified_date(self, file_path):
193
+ """
194
+ Get the last modified date of a file.
195
+
196
+ Args:
197
+ file_path (str): Path to the file.
198
+
199
+ Returns:
200
+ Last modified date of the file.
201
+ """
202
+ return os.path.getmtime(file_path)
203
+
204
+ def write_pdf(self):
205
+ """
206
+ Write the pdf file.
207
+ """
208
+ convert(
209
+ self.md_path,
210
+ self.css_path,
211
+ self.output_path,
212
+ extend_default_css=self.extend_default_css,
213
+ )
214
+ if self.loud:
215
+ print(f"- PDF file updated: {datetime.now()}", flush=True)
216
+
217
+ def observe(self, poll_interval=1):
218
+ """
219
+ Observe the markdown and CSS files. Calls write_pdf() when a file is
220
+ modified.
221
+ """
222
+ self.write_pdf()
223
+
224
+ self.md_last_modified = self.get_last_modified_date(self.md_path)
225
+ self.css_last_modified = self.get_last_modified_date(self.css_path)
226
+
227
+ try:
228
+ while True:
229
+
230
+ md_modified = self.get_last_modified_date(self.md_path)
231
+ css_modified = self.get_last_modified_date(self.css_path)
232
+
233
+ if (
234
+ md_modified != self.md_last_modified
235
+ or css_modified != self.css_last_modified
236
+ ):
237
+
238
+ self.write_pdf()
239
+
240
+ self.md_last_modified = md_modified
241
+ self.css_last_modified = css_modified
242
+
243
+ time.sleep(poll_interval)
244
+
245
+ except KeyboardInterrupt:
246
+ if self.loud:
247
+ print("\nInterrupted by user.\n", flush=True)
@@ -0,0 +1,98 @@
1
+ """
2
+ This module contains functions that are used to get the output path, the CSS
3
+ path, and the usage message.
4
+ Author: @julynx
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ try:
10
+ # Python 3.9+
11
+ from importlib.resources import files
12
+ except ImportError:
13
+ # Fallback for older Python versions
14
+ from importlib_resources import files
15
+
16
+ from .constants import BLUE, CYAN, GREEN, YELLOW, OPTIONS, OPTIONS_MODES
17
+ from .utils import color
18
+
19
+
20
+ def get_output_path(md_path, output_dir=None):
21
+ """
22
+ Get the output path for the pdf file.
23
+
24
+ Args:
25
+ md_path (str): The path to the markdown file.
26
+ output_dir (str): The output directory.
27
+
28
+ Returns:
29
+ str: The output path.
30
+ """
31
+ md_path = Path(md_path)
32
+
33
+ if output_dir is None:
34
+ return md_path.parent / f"{md_path.stem}.pdf"
35
+
36
+ output_dir = Path(output_dir)
37
+
38
+ if output_dir.suffix == ".pdf":
39
+ return output_dir
40
+
41
+ return output_dir.parent / f"{Path(md_path).stem}.pdf"
42
+
43
+
44
+ def get_css_path():
45
+ """
46
+ Get the path to the default CSS file.
47
+
48
+ Returns:
49
+ str: The path to the default CSS file.
50
+ """
51
+ package_files = files("markdown_convert")
52
+ css_file = package_files / "default.css"
53
+ return str(css_file)
54
+
55
+
56
+ def get_code_css_path():
57
+ """
58
+ Get the path to the code CSS file.
59
+
60
+ Returns:
61
+ str: The path to the code CSS file.
62
+ """
63
+ package_files = files("markdown_convert")
64
+ css_file = package_files / "code.css"
65
+ return str(css_file)
66
+
67
+
68
+ def get_usage():
69
+ """
70
+ Returns a message describing how to use the program.
71
+
72
+ Returns:
73
+ str: The usage message.
74
+ """
75
+ commd = (
76
+ f"{color(GREEN, 'markdown-convert')} "
77
+ f"[{color(YELLOW, OPTIONS[0])}] [{color(BLUE, 'options')}]"
78
+ )
79
+ opt_1 = f"{color(BLUE, OPTIONS[1])}{color(CYAN, '=')}{color(CYAN, '|'.join(OPTIONS_MODES))}"
80
+ opt_2 = (
81
+ f"{color(BLUE, OPTIONS[2])}{color(CYAN, '=')}[{color(CYAN, 'css_file_path')}]"
82
+ )
83
+ opt_3 = f"{color(BLUE, OPTIONS[3])}{color(CYAN, '=')}[{color(CYAN, 'output_file_path')}]"
84
+
85
+ usage = (
86
+ "\n"
87
+ "Usage:\n"
88
+ f" {commd}\n"
89
+ "\n"
90
+ "Options:\n"
91
+ f" {opt_1}\n"
92
+ " Convert the markdown file once (default) or live.\n"
93
+ f" {opt_2}\n"
94
+ " Use a custom CSS file.\n"
95
+ f" {opt_3}\n"
96
+ " Specify the output file path.\n"
97
+ )
98
+ return usage
@@ -0,0 +1,38 @@
1
+ """
2
+ Utility functions for string manipulation.
3
+ Author: @julynx
4
+ """
5
+
6
+ import platform
7
+
8
+
9
+ def color(color_code, text):
10
+ """
11
+ Colorize text.
12
+
13
+ Args:
14
+ text (str): The text to colorize.
15
+ color (str): The color code.
16
+
17
+ Returns:
18
+ str: The colorized text.
19
+ """
20
+
21
+ # Disable if running on Windows
22
+ if platform.system() == "Windows":
23
+ return text
24
+
25
+ return f"\033[{color_code}m{text}\033[0m"
26
+
27
+
28
+ def drop_duplicates(lst):
29
+ """
30
+ Drops duplicates from the given list.
31
+
32
+ Args:
33
+ lst: List to remove duplicates from.
34
+
35
+ Returns:
36
+ List without duplicates.
37
+ """
38
+ return list(dict.fromkeys(lst))
@@ -0,0 +1,61 @@
1
+ """
2
+ This module contains functions to validate the input paths.
3
+ Author: @julynx
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+
9
+ def validate_markdown_path(md_path):
10
+ """
11
+ Validate the markdown file path.
12
+
13
+ Args:
14
+ md_path (str): The path to the markdown file.
15
+
16
+ Raises:
17
+ FileNotFoundError: If the file is not found.
18
+ ValueError: If the file is not a Markdown file.
19
+ """
20
+ if not Path(md_path).is_file():
21
+ raise FileNotFoundError(f"File not found: '{md_path}'")
22
+
23
+ if not md_path.endswith(".md"):
24
+ raise ValueError("File must be a Markdown file.")
25
+
26
+
27
+ def validate_css_path(css_path):
28
+ """
29
+ Validate the CSS file path.
30
+
31
+ Args:
32
+ css_path (str): The path to the CSS file.
33
+
34
+ Raises:
35
+ FileNotFoundError: If the file is not found.
36
+ ValueError: If the file is not a CSS file.
37
+ """
38
+ if not Path(css_path).is_file():
39
+ raise FileNotFoundError(f"File not found: '{css_path}'")
40
+
41
+ if not css_path.endswith(".css"):
42
+ raise ValueError("File must be a CSS file.")
43
+
44
+
45
+ def validate_output_path(output_dir):
46
+ """
47
+ Validate the output directory path.
48
+
49
+ Args:
50
+ output_dir (str): The path to the output directory.
51
+
52
+ Raises:
53
+ FileNotFoundError: If the directory is not found.
54
+ """
55
+ check_dir = Path(output_dir)
56
+
57
+ if output_dir.endswith(".pdf"):
58
+ check_dir = check_dir.parent
59
+
60
+ if not check_dir.is_dir():
61
+ raise FileNotFoundError(f"Directory not found: '{check_dir}'")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "markdown_convert"
7
- version = "1.2.14"
7
+ version = "1.2.16"
8
8
  description = "Convert Markdown files to PDF from your command line."
9
9
  authors = [
10
10
  { name = "Julio Cabria", email = "juliocabria@tutanota.com" },
@@ -35,6 +35,12 @@ include = [
35
35
  "markdown_convert/__main__.py",
36
36
  "markdown_convert/default.css",
37
37
  "markdown_convert/code.css",
38
+ "markdown_convert/modules/__init__.py",
39
+ "markdown_convert/modules/constants.py",
40
+ "markdown_convert/modules/convert.py",
41
+ "markdown_convert/modules/resources.py",
42
+ "markdown_convert/modules/utils.py",
43
+ "markdown_convert/modules/validate.py",
38
44
  ]
39
45
 
40
46
  [tool.hatch.build.targets.wheel]
@@ -43,4 +49,10 @@ include = [
43
49
  "markdown_convert/__main__.py",
44
50
  "markdown_convert/default.css",
45
51
  "markdown_convert/code.css",
52
+ "markdown_convert/modules/__init__.py",
53
+ "markdown_convert/modules/constants.py",
54
+ "markdown_convert/modules/convert.py",
55
+ "markdown_convert/modules/resources.py",
56
+ "markdown_convert/modules/utils.py",
57
+ "markdown_convert/modules/validate.py",
46
58
  ]